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.
The next section is intentionally written as a copyable prompt for development sessions. In plain language, the policy says:
sprintf()/gettextf()/condition-helper format
string.Useful background:
issue*_* convention.lintr baseline, configured
exceptions, and deliberately disabled linters.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.
issueXXX_short_description, which
matches the protected issue*_* pattern.devtools::spell_check() was run if user-facing text
changed.devtools::document() was run if Roxygen changed.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.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:
dq_report_by() bundle with top-level
index.html, dashboard.html, and
tables.html;report_*/.report/report.html, including its
dim_*.html and VAR_*.html detail pages;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:
dataquieR/Nexus release path, so the test definition
remains stable while still checking the distributed package.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.soffice --version instead of installing LibreOffice at
runtime.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.The following helper tools support the workflow above:
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.