Purpose

This page summarizes the working policy for dataquieR and QualityIndicatorFunctions development. It can be used as a reusable prompt for development sessions and as a checklist for merge requests.

Human-readable overview

The next section is intentionally written as a copyable prompt for development sessions. In plain language, the policy says:

  • Track user-facing work in the public dataquieR project and implement package changes in the internal QualityIndicatorFunctions project.
  • Use protected issue branches, draft merge requests, focused local checks, and GitLab CI evidence before merging.
  • Keep R package changes small, reviewable, CRAN-conscious, and aligned with existing dataquieR helper APIs and metadata constants.
  • Treat message formatting as a security boundary: package-owned literal format strings are fine, but externally derived text must be passed as data, not as a sprintf()/gettextf()/condition-helper format string.
  • Keep generated documentation, Selenium report fixtures, release checks, and pipeline bookkeeping explicit enough that a later reviewer can reconstruct what happened.

Useful background:

Universal development prompt

You are working on dataquieR / QualityIndicatorFunctions.

Projects and issue flow:
- Public/external project: https://gitlab.com/libreumg/dataquier
- Internal development project:
  https://gitlab.com/libreumg/internal/QualityIndicatorFunctions
- User-facing issues are usually tracked in the public dataquieR project.
- Implementation usually happens in the internal QualityIndicatorFunctions
  repository.
- Internal branches and merge requests must refer back to the public issue.

Branch and merge request convention:
- Use branches named issueXXX_short_description, for example
  issue803_report_without_metadata.
- Use underscores in the issue branch name. The internal project protects the
  `issue*_*` branch pattern so protected runners and protected CI variables are
  only available on correctly named issue branches.
- Open a draft merge request in the internal QualityIndicatorFunctions project.
- The MR description should link the public issue, for example:
  Fixes libreumg/dataquieR#803
  or, if the work is not final:
  Refs libreumg/dataquieR#803
- Do not do the final merge automatically; the final merge is reviewed together
  with Stephan.
- Small, clear CI fixes on the same branch are acceptable. Re-run focused local
  checks before pushing.

Website publication workflow:
- Website publication is maintained by the project team. Protected internal
  workflow documentation exists in
  [the internal website publication workflow](https://gitlab.ship-med.uni-greifswald.de/dfg_website/dfg_website_rendered/-/blob/renders/PROMOTION_WORKFLOW.md)
  (internal link, accessible only for authorized developers).

Code conventions:
- Read the surrounding code first and follow existing local patterns.
- Keep changes narrowly scoped to the issue.
- Internal helper functions use the util_ prefix.
- Do not introduce abstractions unless they remove real complexity or match an
  established package pattern.
- Use `withr` helpers such as `withr::defer()`, `withr::local_options()`,
  `withr::local_dir()`, or focused `withr::with_*()` wrappers for cleanup and
  temporary state. Do not add new `on.exit()` cleanup code.
- Report iframe rendering keeps a centrally managed HTML dependency set for
  parallel flush mode. When adding new iframe-level `htmltools::htmlDependency()`
  requirements, update that central manager and add a focused render test.
- Use the exported metadata name constants whenever they exist. Metadata
  column and attribute names such as `DF_CODE`, `VAR_NAMES`, `DATA_TYPE`, or
  names from `WELL_KNOWN_META_VARIABLE_NAMES` must not be duplicated as string
  literals in implementation code. For example, prefer
  `meta_data[[VAR_NAMES]]` over `meta_data[["VAR_NAMES"]]`, and prefer
  `meta_data[[VAR_NAMES]]` over `meta_data$VAR_NAMES`.
- DQ-OBS metadata cells must use the documented empty-value conventions. Do not
  enter sentinel strings such as `IGNORE` to mean "empty" or "do not show this";
  use an empty cell or `|`, depending on the DQ-OBS field semantics. Package
  code may temporarily tolerate historic `IGNORE` values for compatibility, but
  must not introduce `IGNORE` as a new metadata convention.
- Exported functions must be robust and user-facing errors must be meaningful.
  Avoid raw R errors where a controlled dataquieR condition or explanatory
  message is possible.
- Treat computed format strings as security-sensitive. Calls to `sprintf()`,
  `gettextf()`, and derived condition/message helpers such as `util_error()`,
  `util_warning()`, `util_message()`, their local aliases, and specialized
  wrappers around them must not use user-controlled or derived text as the
  format argument. Prefer literal package-owned formats such as
  `sprintf("%s", msg)` or `util_warning("%s", msg)` when forwarding already
  constructed text, and keep static tests that distinguish trusted package
  literals from externally derived input, including indirect cases such as
  `problem <- "bad"; sprintf(problem)`.
- Unexpected failures should be reported in a way that helps users contact the
  maintainers with enough context.
- Do not call base `stop()`, `warning()`, or `message()` directly in package
  implementation code. Use `util_error()`, `util_warning()`, and
  `util_message()` so conditions carry the expected dataquieR classes, metadata,
  and rendering behavior. Direct base calls are allowed only inside the
  implementation of these wrapper helpers or in a narrowly justified
  compatibility shim.
- Do not edit the external dataquieR repository directly when the internal
  QualityIndicatorFunctions repository is the upstream source.
- Do not change versions, NEWS, release notes, tags, or releases unless release
  work was explicitly requested.

Linting policy:
- QualityIndicatorFunctions uses `lintr` via the internal `lint_qif` CI job.
  Treat lint results as review feedback for readability, maintainability, and
  small correctness risks; do not apply mechanical lint edits that make the code
  harder to understand or change semantics.
- The full linting policy is documented on the website at
  dataquieR-linting-policy.html.
- The active lint configuration intentionally uses QIF-specific exceptions for
  domain names, result slot names, metric abbreviations, metadata constants,
  REDCap rule language names, S3 methods, JavaScript/DataTables option names,
  and historic helper APIs that are not free to rename.
- The maximum source line length is 80 characters. Use focused `# nolint`
  comments only for local exceptions that would become less readable or less
  faithful to external/domain-specific text if wrapped.
- Do not keep commented-out R code in source files. When removing historical or
  experimental code during lint cleanup, replace it with a short explanatory
  comment only if the location or decision is still relevant. Prefer the local
  pattern `Historical <topic> removed here. Inspect with git show <commit> --
  <path> before restoring.` so future work can find the removed implementation
  in Git history without preserving stale code in comments.
- `object_length_linter` is configured with a higher threshold than the lintr
  default. Very long new names should stay exceptional, but QIF's descriptive
  option names, report slots, and domain/API terms are often necessarily longer
  than 30 characters.
- QIF currently keeps magrittr pipes (`%>%`) as the consistent package style.
  Do not mix in native pipes (`|>`) opportunistically. A native-pipe migration,
  if desired, should be reviewed as a separate semantic style migration.
- `return_linter` is deliberately disabled. Use explicit `return()` when it
  improves readability, for example for early exits or control-flow-heavy code.
  In short lambda-like helper functions, an implicit return is often clearer;
  remove `return()` there when it reads better.
- `object_usage_linter` is deliberately disabled. It is a codetools-style check
  related to R CMD check "no visible binding" warnings and produces many false
  positives for QIF's non-standard evaluation, data masks, tidy evaluation,
  generated metadata environments, REDCap/report domain-specific languages, and
  code such as `subset(cars, speed > dist)` where names are resolved in a data
  context. Fix true findings locally when they are obvious, but avoid broad
  `utils::globalVariables()` suppressions. Such global exceptions hide real
  problems and can make the check ineffective.
- For a potentially multidimensional `[` subset, always state the `drop`
  argument. Use `drop = FALSE` when later code requires a data frame, matrix,
  or array; use `drop = TRUE` only when vector/scalar simplification is the
  deliberate contract. Base R otherwise simplifies a one-column data-frame
  subset silently. QIF's early lint check prevents new omitted `drop`
  arguments while the historical inventory is audited in reviewed batches.

Roxygen and generated artifacts:
- If Roxygen documentation changes, run devtools::document().
- Roxygen updates can regenerate inst/manual.RData and
  inst/indicator_or_descriptor.RData.
- These binary artifacts may be committed to the branch that regenerated them.
- After merges that touch Rd files, document() may need to be run again on the
  target branch, because these generated artifacts can change.

Generated package metadata and compressed artifacts:
- Some files under `inst/` are generated package artifacts and should not be
  edited manually when a generator exists.
- `inst/update_from_DQ_OBS.R` refreshes DQ_OBS-derived package data, including
  `inst/dqi.rds` and `inst/implementations.rds`. It also sources
  `inst/computed_vars_ind_mapping.R`, which writes
  `inst/computed_vars_ind_mapping.rds` and `inst/ssi.rds`.
- Other update scripts, for example `inst/mimetypes/update.R`, update local
  lookup artifacts for their respective subdirectories.
- The compression helpers `inst/compress_rds.R`, `inst/compress_rdata.R`, and
  `inst/compress_xlsx.R` recompress package data files shipped under `inst/`.
  Run them only when intentionally updating generated or bundled artifacts, and
  review the resulting binary-file changes before committing.

CRAN policy and tests:
- CRAN-active tests must not depend on unstable network access, external
  services, fragile snapshots, or long-running system state.
- Use skip_on_cran() or targeted skip logic for tests that are slow, online-only,
  environment-specific, vdiffr-based, or otherwise fragile.
- CRAN restricts parallel execution; do not require more than two cores in tests.
- If we want to detect unstable upstream changes early, prefer CI/MR pipelines
  over CRAN-active tests.
- Avoid adding CRAN-active tests that can fail because of dependency internals,
  platform rendering, or hash details outside our control.

Local checks before pushing:
- Run the smallest meaningful focused test first, for example:
  devtools::test(filter = "some_context", stop_on_failure = TRUE)
- If documentation changed, run devtools::document().
- If user-facing text, NEWS, or documentation changed, run
  devtools::spell_check().
- For broader changes, consider:
  devtools::check(args = "--no-tests")
- Keep generated reports, logs, and example artifacts in an obvious location if
  Stephan should review them locally.

CI/CD handling:
- Watch GitLab pipelines after pushing branches or starting releases.
- Retry transient runner, network, or timeout failures if appropriate.
- If Nexus is full and access is available, clean up cautiously; otherwise inform
  Stephan.
- Fix small, clear spell-check, Roxygen, or focused test failures directly on the
  branch.
- If the failure is unclear or risky, stop and inform Stephan.
- A merge request should only be accepted when the measured test coverage does
  not decrease by more than 0.1 percentage points compared with the current
  master baseline. In formula form:
  `Coverage(master) - Coverage(MR branch) <= 0.1 percentage points`.
  The 0.1 threshold is fixed by CI policy and is not a pipeline input.
  If coverage evidence is unavailable, stale, or ambiguous, start a deliberate
  `RUN_COVR=TRUE` pipeline before final review. The coverage-gate job stays
  manual while an MR is being developed, so missing evidence does not turn the
  ordinary MR pipeline red. Before merge, a maintainer starts that blocking job
  after the same-SHA `RUN_COVR=TRUE` pipeline is successful. A maintainer may
  override this rule for an urgent merge with
  `COVERAGE_GATE_OVERRIDE=TRUE` and a non-empty
  `COVERAGE_GATE_OVERRIDE_REASON`. The gate records that reason automatically
  in the merge request; do not enter it a second time as a manual comment.

Coverage-gate merge procedure:

Step 1. Finish the normal MR pipeline first. The manual `coverage_gate` job is
intentionally not started while implementation is still in progress.

Step 2. In GitLab, use **Run pipeline** for the MR source branch and set the
pipeline input `RUN_COVR` to `TRUE`. Wait for that same-SHA pipeline's
`test_image:default` job to succeed and publish `coverage_value.txt`. The
equivalent command-line shortcut is:

    glab ci run -b issue948_result_slot_and_value_label_guards \
      --variables RUN_COVR:TRUE

Replace `issue948_result_slot_and_value_label_guards` with the MR source
branch. Before starting this deliberate coverage run, check whether an
equivalent `RUN_COVR=TRUE` pipeline already exists for the same branch and SHA,
because coverage jobs are long-running and should not be duplicated.

Step 3. Return to the ordinary MR pipeline and use the play button beside the
blocking manual `coverage_gate` job. It fetches the same-SHA coverage evidence,
compares it with master, and passes when the drop is at most 0.10 percentage
points. A passing job is the coverage evidence for the subsequent merge.

Step 4. For an urgent exception, a Maintainer starts a source-branch pipeline
with `COVERAGE_GATE_OVERRIDE=TRUE` and a concise
`COVERAGE_GATE_OVERRIDE_REASON`. The job verifies Maintainer access and writes
the reason to the MR automatically; no duplicate MR comment is needed. This is
an exception path, not a way to change the fixed threshold.

- Every internal QualityIndicatorFunctions merge request requires current
  Selenium evidence before merge, independent of whether the changed files match
  the automatic Selenium `changes` rules. The automatic push/MR pipeline should
  run Selenium only for changes with likely report/browser impact, such as
  JavaScript, CSS, HTML, report-rendering code, or explicit
  `RUN_SELENIUM=TRUE` pipelines. This is a cost-control rule, not the merge
  criterion. Before merging, a maintainer starts the blocking manual
  `selenium_gate` job. It accepts either successful Selenium jobs from the same
  pipeline or from another successful same-SHA pipeline. The required evidence
  is `selenium_report` plus all 12 browser jobs:
  `DT`/`DT2` x Chrome/Firefox/Edge x `file`/`http`.

Selenium-gate merge procedure:

Step 1. First check whether Selenium is already running or already green for
the current MR-head SHA. Do not start a second Selenium pipeline only because
Selenium evidence is required for merge.

Step 2. If there is no current same-SHA Selenium evidence, deliberately start a
Selenium-only pipeline for the MR source branch with `RUN_SELENIUM=TRUE`,
`RUN_BUILD=FALSE`, and `RUN_R_TESTS=FALSE`. Before starting it, check whether
an equivalent same-branch and same-SHA Selenium pipeline already exists,
because browser jobs are long-running and should not be duplicated. The
command-line shortcut is:

    glab ci run -b issue948_result_slot_and_value_label_guards \
      --variables RUN_SELENIUM:TRUE \
      --variables RUN_BUILD:FALSE \
      --variables RUN_R_TESTS:FALSE

Replace `issue948_result_slot_and_value_label_guards` with the MR source
branch.

Step 3. Return to the ordinary MR pipeline and use the play button beside the
blocking manual `selenium_gate` job. It passes when it finds same-SHA success
for `selenium_report` and all 12 browser jobs. A passing job is the Selenium
evidence for the subsequent merge.

Step 4. The manual `trigger_selenium_pipeline` job is only a convenience
trigger for deliberate Selenium runs. Do not use it when the current pipeline
already has Selenium jobs running or when a same-SHA Selenium pipeline already
exists, unless Stephan explicitly asks for an additional run.

- Do not delete or stop pipelines, tags, releases, or artifacts unless this is
  clearly part of the requested cleanup.
- For vdiffr snapshot work, use the internal
  libreumg/internal/vdiffr-snapshot-manager helper where possible. It can
  download GitLab job artifacts, collect render-stack metadata, run local
  Docker-based checks, start vdiffr::snapshot_review(), and run temporary
  remote snapshot pipelines via `bin/test-current-snapshot-remotely.sh`.
- CI container snapshots are authoritative. Snapshots generated on developer
  machines are useful diagnostics, but they must not be accepted only because a
  local run produced `.new.svg` files. Accept snapshot changes only when the
  corresponding GitLab CI container/image for the relevant `R_VERSION` produces
  the same change, or when a deliberate rendering-stack migration is being
  reviewed.
- For maintaining R package CI/CD projects based on ci-scripts and ci-templates,
  use the manageGitlabRProjects package from ~/git/gitlab or Nexus.

Release and beta policy:
- Start alpha, beta, or release pipelines only when explicitly requested.
- Follow R versioning conventions, e.g. 2.8.10.9001 for a beta/development
  version.
- Use RELEASE_TAG=vX.Y.Z... deliberately.
- `RELEASE_TAG` may include the human-readable release title after the version,
  e.g. `v2.8.10.9001 New Report Rendering and faster computation`. The first
  whitespace-separated token is the version tag; the full value can still be
  used as commit message and release title.
- Before rerunning a release/beta pipeline for the same version, check and clean
  duplicate tags, releases, and Nexus artifacts where needed.
- Follow downstream pipelines started by external dataquieR, including child
  pipelines, dataquieRlauncher, rstudio/docker/rstudio, DockerHub and
  Nexus-related builds.
- On GitLab Free/Community Edition, release authorization should not rely on
  Protected Environments. Use protected default branches, protected release
  secrets, a manual release gate, and the separate
  [qif-release-policy](https://gitlab.com/libreumg/internal/qif-release-policy)
  repository instead.
- The release pipeline must check all of the following before syncing to the
  external dataquieR repository:
  - it runs on the default branch;
  - `CI_COMMIT_REF_PROTECTED` is `true`;
  - protected release secrets such as `RELEASE_TARGET` and `RELEASE_SSH_FILE`
    are present;
  - `GITLAB_USER_LOGIN` is listed in
    `qif-release-policy:qif-release-allowed-users.txt`;
  - the current QIF `.gitlab-ci.yml` SHA-256 hash matches
    `qif-release-policy:qif-gitlab-ci.sha256`;
  - the version encoded in `RELEASE_TAG` matches `DESCRIPTION`;
  - the release version is either `Major.Minor.Patch` or `Major.Minor.Patch.900N`
    and not an alpha version such as `Major.Minor.Patch.900N.K`.
- The policy repository contains:
  - `qif-release-allowed-users.txt`: GitLab usernames allowed to start QIF
    releases, one per line;
  - `qif-gitlab-ci.sha256`: reviewed hash of the QIF `.gitlab-ci.yml`;
  - `scripts/update-qif-ci-reference.sh`: helper to update the hash after an
    intentional QIF CI change has been reviewed.
- The QIF job token must be allowed to read the policy repository. Configure the
  policy project allowlist with numeric project IDs.

Allowlist setup:

    QIF_PROJECT_ID=10015470
    POLICY_PROJECT_ID=84252717

    glab api \
      --method POST \
      "projects/${POLICY_PROJECT_ID}/job_token_scope/allowlist" \
      --field target_project_id="${QIF_PROJECT_ID}"

    glab api "projects/${POLICY_PROJECT_ID}/job_token_scope/allowlist" \
      | jq -r '.[] | [.id,.path_with_namespace] | @tsv'

To approve an intentional QIF `.gitlab-ci.yml` change:

    cd /Users/struckmanns/git/gitlab/qif-release-policy
    scripts/update-qif-ci-reference.sh /Users/struckmanns/git/gitlab/QualityIndicatorFunctions
    git diff
    git add qif-gitlab-ci.sha256
    git commit -m "Update QIF CI release policy reference"
    git push
- Be honest about the Free/Community Edition limitation: a QIF maintainer can
  still remove the policy check from QIF `.gitlab-ci.yml`; the external policy
  repository is a guardrail against accidental or casual changes, not a
  cryptographic root of trust. Put the policy repository in a group/subgroup
  with a deliberately small maintainer set. For stronger selected-subgroup
  authorization, use GitLab Premium/Ultimate Protected Environments.
- QIF-specific operational note: as of the July 2026 audit, `master` is
  protected for maintainer-only push and merge. `RELEASE_SSH_FILE` is protected,
  but `RELEASE_TARGET` was not protected and should be made protected before
  relying on the release gate. The initial qif-release-policy project was
  created under `libreumg/internal`; because group maintainers inherit project
  maintainer rights, it should be moved to a narrower group/subgroup if the
  inherited maintainer set remains too broad.

dataquieR-specific behavior:
- Grading rulesets should be applied as late as possible, preferably during
  rendering/plotting, so already computed result objects can still be displayed
  differently when rulesets change.
- If several grading rules classify the same contradiction rule, the worst
  class, i.e. the highest class, wins.
- Keep item-level and cross-item-level logic separate.
- Intrinsic applicability problems are expected non-applicability, not ordinary
  crashes. They should render neutrally but keep their explanatory condition
  message.
- Missing or not-requested assessments should be understandable in reports.
- Robustness of exported functions is more important than performance shortcuts.
  Performance improvements must not reduce controlled behavior or explanatory
  messages.

Working style:
- Use small, reviewable commits.
- Do not revert unrelated work in a dirty worktree.
- Commit only files that belong to the current task.
- Prefer focused tests and concrete local reproductions.
- Keep status updates short and factual: what was checked, what changed, and
  what remains.

Checklist for internal merge requests

  • Branch follows issueXXX_short_description, which matches the protected issue*_* pattern.
  • Draft MR exists in the internal project.
  • MR links the public dataquieR issue.
  • Local focused tests are listed in the MR.
  • devtools::spell_check() was run if user-facing text changed.
  • devtools::document() was run if Roxygen changed.
  • CRAN-active tests are stable and do not require network access, fragile snapshots, or more than two cores.
  • The manual selenium_gate job passed for the current MR-head SHA. It may use Selenium jobs from the same pipeline or another successful same-SHA pipeline; do not trigger duplicate Selenium just because the merge checklist requires Selenium evidence.
  • Final merge is left for joint review.

Selenium report-fixture testing

The browser-level report tests live in the internal dataquier_seleniumtest project and exercise the rendered HTML reports produced by dataquieR. They are intended as CI/MR tests, not as CRAN-active package tests. The standard matrix runs the same report interactions for the DT and DT2 table backends, for all supported browsers, and for both direct file:// access and served HTTP access.

The required fixtures are part of the test contract. A standard CI run must create:

  • a dq_report_by() bundle with top-level index.html, dashboard.html, and tables.html;
  • the bundled single report under report_*/.report/report.html, including its dim_*.html and VAR_*.html detail pages;
  • a standalone dq_report2() output with top-level index.html redirecting into .report/report.html.

If these standard files are missing, the affected Selenium test should fail with a clear fixture error. It should not silently skip, because the reports are generated by the pipeline before the browser tests start. Skips are reserved for genuinely optional developer-machine capabilities, such as LibreOffice for the XLSX-to-PDF colour-rendering check, or for local ad-hoc runs where DQ_REPORT2_DIR was not set and the standalone-vs-bundled assertion would be invalid.

The main report cases are:

  • case1 and case1b: dq_report_by() overview, sunburst interactions, managed popups, popup history, dragging/resizing, and opening result pages.
  • case2 and case2b: dashboard table behaviour, SearchBuilder defaults, column filtering, sorting colours, clearing filters, and variable links. For DT2, the tests use the newer ColumnControl filter UI while keeping the expected behaviour equivalent to the legacy DT tests.
  • case3 and case3b: summary tables, tippy content, classification colours, column-visibility groups, total columns, and Copy/CSV/PDF/XLSX/Print/PNG exports.
  • case3c: XLSX exports from a per-variable VAR_*.html summary table preserve grading-palette cell backgrounds. The test prefers a known grading-coloured page and otherwise scans the generated VAR_*.html pages; a missing grading-coloured page is a fixture error in CI.
  • case3d: embedded-report navigation. A direct showDataquieRResult() call from inside an iframe must open a sibling managed popup in the top-level popup manager; generated back-to-overview links must reuse the managed iframe; ordinary variable links with target="_top" must navigate the top-level page rather than nesting another dialog.
  • case4: the bundled single report inside a dq_report_by() bundle. It checks the navbar, dropdowns, window.dq_report2, the visible “Back to reports’ overview” link, overview links from the top-level bundle into report_*/.report/, single-page view via #nm=..., and sorting/searching of N (%) columns.
  • case5: standalone dq_report2() output. It checks the redirect from top-level index.html, absence of the back-to-overview link, report marker flags, dropdown content, safe generated VAR_*.html names and anchors, and sorting/searching of standalone N (%) columns.
  • case6: navbar keypress search in a bundled single report.
  • case7: dashboard exports and whether exported rows respect the active dashboard filters.

The intent is that DT2 tests are behaviourally equivalent to the DT tests even where the GUI differs. Test helpers should therefore model the user’s observable action, such as opening the appropriate column-filter control, rather than depending on a backend-specific internal widget structure.

Detailed maintenance notes are kept in TESTS.md in the internal Selenium project. The following points are part of the developer contract:

  • The Selenium project should define the tests and be runnable explicitly, but green QIF or dataquieR pipelines from protected/trusted refs are the authoritative acceptance gates. Feature branches in the Selenium project or other CI-helper projects must not be consumed as trusted test definitions by unrelated projects unless the ref is protected or otherwise reviewed.
  • The standalone Selenium fixture should be generated from current QIF top-level code for development feedback. It should also cover the current beta package installed from the external dataquieR/Nexus release path, so the test definition remains stable while still checking the distributed package.
  • Standalone dq_report2() output must ship the favicon asset in every relative context that references it: top-level index.html expects logo.png next to itself, and pages below .report/ expect .report/logo.png. Browser tests should treat missing logo.png as a real fixture or product failure, not as a filtered console warning.
  • LibreOffice is required only for the visual XLSX colour-rendering check. It should be baked into the browser-test image together with the Cypress browser stack, and the job should verify soffice --version instead of installing LibreOffice at runtime.
  • XLSX colour rendering is checked on a per-variable VAR_*.html summary table, not on the top-level tables.html export. The latter can preserve workbook style definitions without applying the grading fills to cells, so it is not a sufficient visual regression target.
  • target="_top" links inside embedded report iframes should navigate the top-level page. Managed report popups are tested separately and should not be confused with those ordinary top-level links.

Developer helper tools

The following helper tools support the workflow above:

  • vdiffr-snapshot-manager: internal helper for reviewing and accepting vdiffr snapshots. It can download GitLab CI artifacts, copy .new.svg files into a local checkout, start vdiffr::snapshot_review(), run snapshot tests inside Docker CI images, write render-stack metadata, and run bin/test-current-snapshot-remotely.sh to start a temporary remote pipeline for the current snapshot state. That shell workflow downloads all relevant artifacts locally and deletes the temporary pipeline afterwards. Use it when the question is whether the remote CI image produces the same snapshot output as a local run. The CI image is the source of truth for accepting snapshots; local developer-machine output is only supporting evidence. Snapshot acceptance and pushing are explicit actions.
  • manageGitlabRProjects: R package for creating and maintaining R packages that use the shared ci-scripts and ci-templates GitLab CI/CD setup. The package is available in ~/git/gitlab and in Nexus.

For snapshot provenance, CI jobs should store vdiffr-render-metadata.json as an artifact and print the compact render-stack summary to the job log. This file should include at least R, platform, relevant graphics packages, CI image/tag, and discoverable system graphics libraries.