Changelog
Source:NEWS.md
lissr 1.4.1
Correctness fixes for data-file discovery and validation of required inputs. Checks now detect unresolved targets and wave scopes before evaluating their values. Error-level failures or unevaluable checks stop strict-mode output; the default report mode writes output with valid_for_analysis = FALSE.
Previously accepted recipes that omit required targets or scopes may now report unevaluable checks. Consult the canonical schema for each check’s supported fields and empty-data behavior. General value/bound payload validation and comprehensive empirical verification remain outside this release’s guarantees.
The packaged canonical-schema vignette now includes rendered HTML, extracted R code and its vignette index entry. It is available through
vignette()andbrowseVignettes()without rebuilding, and tests detect stale schema renders.row_countandassert_row_count_rangenow require an explicitly requested wave to exist and its row membership to be known. Missingwave_id, absent waves and malformed scopes are unevaluable with the declared severity, instead of potentially passing as zero rows. Global counts still allow empty data without wave identifiers. Exact field lookup prevents unintended partial matches; scalar wave conversion and inclusive bound comparisons are preserved.wave_countandn_distinct_wavenow require the inputs used by their selected counting mode and preserve severity for unevaluable checks. Global expected counts need onlywave_id; per-person limits also require one exact person-key column. Field lookup no longer partially matches unrelated names, and named scalar keys no longer rename grouping columns. Existing NA/blank counting and scalar count coercion are preserved. Empty per-person scopes report that no counts were calculated, without the former empty-maximum warning. ThecsV07 description now matches its existing 18-wave setting.uniquenessand its aliases now require every selected key column to resolve. Missing keys and malformed active declarations are unevaluable with the declared severity, instead of checking a partial key or passing without keys. Positional key lists longer than two are explicitly unsupported; compound keys remain available throughcolumnandwithin. Existing key precedence, shorthand aliases, duplicate counts, NA/blank grouping and valid empty-data behavior are preserved.per_wave_meannow requires every target to resolve and every row to have known wave membership. Missing inputs and malformed targets are unevaluable with the declared severity. Valid empty data and non-finite means retain their existing outcomes, with diagnostics identifying uncalculated or uncompared means. Target lookup, numeric coercion, inclusive bounds and separate comparisons for every target in every observed wave are preserved.na_rateand its aliases now require every target and requested wave to resolve before applying conditions. Missing inputs and malformed scopes are unevaluable with the declared severity. Scalar/list"all"scopes are normalized. Valid filters selecting no rows retain their pass result with a diagnostic stating that no rate was calculated. Item ranges, pooled NA rates, threshold defaults and safe condition evaluation retain their behavior.value_presentandvalue_present_per_wavenow require every requested target and wave to resolve. Missing inputs, malformed scopes and unavailable wave membership are unevaluable with the declared severity. Omitted and scalar/list"all"scopes check every observed wave; empty data are unevaluable. Each wave still needs a matching value in any selected target. Social Integration V02 now preserves its zero-padded target001as a string.structural_missingnessand its aliases now resolve every required target and wave scope before testing values. Missing inputs and malformed scopes are unevaluable, preserving severity and strict/report output handling. All-NA scopes, per-wave expected presence and expected-present complements support scalar/list"all". Presence-only checks no longer inherit an all-NA scope through partial field-name matching. Health CHK04/CHK07 declare all-NA scopes for their existing derived outputs; Social Integration V08 preserves target002as a string. These changes affect validation, not harmonization rules.value_absenceand its aliases now require resolved targets and wave scopes in every block. Missing inputs and malformed scopes are unevaluable; explicit missing block targets no longer fall back to parent targets. Valid parent and block filters intersect, andwaves_allowedchecks the validated complement. Exclusion precedence, numeric/character matching and legitimate empty/all-NA checks are preserved. Health CHK05 and Religion VC04/VC05 now declare explicit targets, with the Religion checks scoped to their existing recode rules.value_rangeandvalue_in_setchecks now report missing required columns and requested waves as unevaluable, including partial target matches. Invalid target/wave scopes retain the declared severity and diagnostic details. Range checks now honor wave filters; both checks normalize scalar and list"all"scopes. Error-level unevaluable checks block strict output and invalidate report-mode output. Existing aliases, all-NA behavior and other validators retain their behavior.Phase-6
expected_presencechecks now fail for missing required columns, absent waves and all-NA wave values. Malformed or empty requests and missing wave identifiers are unevaluable, preserving severity. Error-level failures and unevaluable checks block strict output and invalidate report-mode output. Checks require an exact column name and explicit waves or"all"; the separateglobal.expected_presencerules and other validation executors are unchanged.Wave discovery filters primary, fallback and auxiliary candidates to existing supported data files before release selection. Codebooks, directories and dangling symlinks no longer become inputs or suppress fallback matches. Release ranking, ambiguity errors, auxiliary separation and release pins retain their existing behavior, with mixed-folder regression coverage.
lissr 1.4.0
A nine-stage, evidence-driven overhaul of the merge engine, the bundled recipes, the income cleaner, the download and authentication layer, and the documentation. Every stage landed with its own regression tests and a green R CMD check; the test suite grew from 448 to roughly 760 assertions. The stage-by-stage record follows the highlights.
Highlights:
- Merge engine correctness:
fieldwork_ymis populated across the nine declaring modules (it was blanked by an ordering bug), cross-wave type harmonization is factor-safe, and every rule application leaves an audit trace. - Recipe conformance, made permanent: per-action payload validation driven by the vocabulary registry, a check-type grammar under which all 99 bundled validation checks classify as 68 executable and 31 documentary (none silently skipped), and an exported
audit_liss_recipes()corpus report suitable for CI gating. - End-to-end assurance: every bundled recipe merges a synthetic panel in the test suite with strict assertions; merged outputs carry provenance (versions, input md5 hashes, release decisions) and an explicit
valid_for_analysisverdict; release-version pins and an overwrite guard protect outputs. - Output-changing semantic repairs, each verified against archive evidence: the cf module rebuilt from a 17-wave scan (exact recode counts reproduced on real data), ca’s
wave_yearstandardized to the fieldwork year with the asset reference year preserved per respondent, cv’s fieldwork months made executable, and cr’s 2019 scale breaks expressed as era-scoped output columns. - Income cleaning: eight policy defects repaired (anchor dispatch, range-fallback voiding, single-person and zero-gross gates, bracket mapping, household-id fallback with join diagnostics, scoped
enable_only, configurable F01 disposition, equivalisation guards), ruleset 1.1.0, controlled-value validation, and an adversarial fixture suite. - Download and authentication hardening: HTTP errors are never written as data, verified unzip protects sole copies, session expiry aborts batches, downloads stream with timeout and retries, credentials are never echoed, 2FA is retryable and guarded, the login probe is cheap and RNG-neutral, and keyring moved to Imports.
- Documentation rebuilt against reality, with drift tests that re-derive every factual claim from the code and recipes, and a canonical-schema article rendered from the packaged schema at build time.
Stage 8: documentation overhaul
The documentation now describes the package the preceding stages built, and a drift-test suite (tests/testthat/test-stage8-docs.R) re-derives every factual claim from the code and the bundled recipes so the two cannot diverge silently again.
- Corrected content: the Background Variables file is no longer called “module (CA)” anywhere (it is a separate monthly release; ca is Assets); every “eight core modules” claim now says ten and the batch examples include ca and cr; the multi-module download loop no longer derives directory names by truncating display names (a bug that wrote Health files into
data/he/) and matches modules by name fragment instead of exact display strings; the longitudinal vignette’s fabricated columns (s002as age,s038as BMI) and its undefinedtreatedvariable are replaced by real recipe-declared flag columns (ch_ecig_dental_items_present,s020_wording_period, and the cancer/alzheimer era flags) and an explicitly reader-supplied treatment definition; the reproducible-pipelines CI chunk installs lissr from GitHub instead ofinstall_local(".")on a non-package research project. - Settled facts: avars files carry their fieldwork period as a
wavecolumn (YYYYMM), so the merge-workflow example reads it directly instead of parsing filenames (the multi-module vignette already did); wave counts and year lists reflect the current recipes (ch has 18 waves, ch07a-ch25r, skipping 2014; cv skips 2015 and reaches cv26r; cw has 15); thestr(recipe)andVALID_ACTIONSlistings show the real current output; the documented schema version is v1.1.0 everywhere (the packaged schema’s own version), with the additive relationship to v1.0.0 stated. - Additions: a new “The Canonical Recipe Schema” article renders
inst/schema/CANONICAL_SCHEMA.mdat build time (zero duplication, no drift); the README gains the income-cleaning and schema articles in its vignette list, a directory-and-naming-contract section, the keyring-ships-with-lissr note, provenance/valid_for_analysis andaudit_liss_recipes()coverage, and module labels matching the recipes; the pkgdown reference index now listsaudit_liss_recipes()(its absence would have failed the site build) and the new article. - Drift tests: no “(CA)” mislabel and no eight-module claim anywhere; batch lists must contain exactly the ten module codes; the action vocabulary and
str()listings in custom-recipes must equal the installed package’s reality; flag names quoted in vignettes must exist in the recipes; wave-count claims must match the recipes’ wave_index lengths; the documented schema version must match the packaged schema. README assertions run in a source tree and skip under R CMD check.
Stage 7: downloader and authentication hardening
The network layer no longer reports failure as success, no longer destroys data on failure, and fails loudly and diagnosably when the archive’s pages drift. The I/O surface is reduced to three small shims, and every error path is exercised offline by mocked tests (tests/testthat/test-stage7-network.R).
- N1, download integrity: HTTP error responses are detected by status code and never written to disk as data files (a 404 page previously became
foo.savwith status “ok”); ZIP archives are extracted with verification and the archive is deleted only after the extracted files exist (a corrupt zip previously triggered deletion of the only copy); the first session expiry aborts the remaining batch with statusskipped_batch_abortedinstead of failing file by file. - N5, transfer robustness: downloads stream to disk through the session’s cookie handle (no whole-file buffering in memory), carry a per-file
.timeout, retry transient failures (curl errors and HTTP 5xx, never deterministic 4xx) with.retries, and can skip files already present via.skip_existing. Partial transfers land in a temporary name and are renamed only after the status check. - N2, confirmation logic: the “download everything?” prompt now defaults to NO, is skipped when an explicit selection tibble was passed, and a non-interactive full-archive request aborts with guidance instead of silently auto-confirming.
- N3, credential hygiene: the failed-login message no longer echoes a masked password (first character plus stars); an empty or unreadable keyring password aborts the login with a re-store hint instead of crashing on
strrep(). - N4, form discovery: a login page without the expected form or username/password fields aborts with a diagnosis naming what was found, instead of a subscript-out-of-bounds error.
- N6, content-disposition: header parsing supports the quoted, unquoted, and RFC 5987
filename*=forms (the unquoted form previously yielded the whole header as the file name), and every resulting name is sanitized (path components stripped, traversal and reserved characters rejected). - N7, blueprint cache: failed module and wave pages are tallied and reported; a partial scrape caches with an explicit incompleteness warning, an empty scrape aborts and caches nothing, and an unreachable module index aborts immediately.
- N8, two-factor entry: the code is whitespace-normalized (email copy-paste), a rejected code can be retried up to three times without restarting the login, a kicked-back session aborts with guidance, and non-interactive sessions abort before prompting (previously an empty string was submitted silently).
- N9,
liss_is_logged_in(): the probe is now cheap and side-effect free. With a cached blueprint it HEADs the first known protected file path (no body transfer, deterministic choice); without one it inspects the login page. It no longer triggers a full archive scrape, downloads a random data file, or mutates the RNG state. - N11, keyring placement:
keyringmoves from Suggests to Imports; it was already unconditionally required for credentialed use, and the install-time guards pretending otherwise are gone (with them the three permanently-skipping guard tests).
Stage 6: income-cleaning policy repairs (ruleset 1.1.0)
Eight policy defects in liss_clean_income() that could destroy or fabricate values on real data are repaired, the ruleset validator now enforces controlled values, and an adversarial fixture suite pins every repair (tests/testthat/test-stage6-cleaning.R).
- C1:
selection.anchoris now executed, not merely reported. The engine dispatches the candidate-selection statistic (household_medianorhousehold_mean) from the ruleset’sselectionblock, so the generated report can no longer claim a method the code did not use; unknown anchors are rejected at load time. - C2: the
range_midpointcorrection fallback (C06) additionally requires the anchor itself to lie inside the admissible range. A below-minimum household (e.g. 5000/5200/50000/5100 againstmin_income8000) previously had its 10x entry error “corrected” to the range midpoint 79000, fifteen times the truth, ledgered as a confident correction; such cells are now voided with an explicit justification. - C3: D04 (personal-income echo) fires only in households known to hold at least
min_household_size(default 2) members, resolved from the newvariables.household_sizemapping (aantalhh); in a single-person household the echo is the expected truth, and roughly 40 percent of Dutch households are single-person. D03 (contextual floor) requires a positive gross personal income instead of merely a finite one, so “household income near zero while gross personal is 0” no longer voids. - C4: bracket-code expansion (P04) classifies the category column by the share of nonzero finite values in 1..max_code (new
code_shareparam, default 0.9) and maps per value. One stray sentinel no longer silently disables mapping for the whole column (after which raw codes were misread as euro bounds and D07 was disarmed); stray codes are warned about and treated as missing brackets, and ambiguous mixtures are declined loudly. - C5: rows with a missing household id fall back to person-id grouping for the household stage instead of silently receiving no cleaning; skipped and fallback row counts appear in the summary, log, and report. The background join (P01) detects the background wave scale (yyyymm, calendar year, or annual wavenr) instead of assuming yyyymm, reports its match rate, and warns when nothing matches (a year-keyed background previously produced an all-NA join logged as success).
- C6:
enable_onlyis scoped per ruleset section: only sections containing a named rule are restricted, soenable_only = "D06"isolates one detection rule while preparation, correction, and finalization machinery keep running (previously the detected cell was voided because its correction rules were disabled too). - C7: the F01 hard cap gains a
disposition:void(default, the old behavior),winsorise(over-cap values clamp to the cap; non-positive values still void), orflag(ledger only, values retained). Genuine top incomes exist, and unconditional voiding biases the right tail downward; the choice is now explicit, per-ruleset or per-call. - C9: equivalisation guards: zero-adult compositions yield NA (the OECD-modified divisor could fall below 1), composition vectors of intermediate length are an error instead of silently recycling, and the documentation states that
aantalki(children at home of any age) only approximates the OECD under-14 child definition. - A8:
validate_cleaning_ruleset()enforces controlled values for detection-ruledisposition/scope/stage, the finalization disposition, the selection anchors, consensus-detectormethods(withconsensusbounded by the method count), and nested per-methodthresholds. - Audit consistency: D01’s ledger
variablefield no longer renders as the literal “NA/…” when a bounds column is unresolved; P03, D01, and the F01 dispositions emit proper JSONL log entries; per-rulelog: falsenow suppresses that rule’s per-rule decision aggregation in the JSONL trace (the decision ledger itself is never suppressed);liss_cleaning_report()requiresoutput_dirinstead of writing three files into the working directory by default. - Tests: three cli-rendered assertions were width-fragile (multi-word regexes break when cli wraps between words) and now match across wrap points.
Stage 5b: output-changing recipe semantics (ca, cv, cr)
- ca
wave_indexyears now mean the same thing as in every other module: the FIELDWORK year. They previously held the asset REFERENCE year (one year earlier), sowave_yearin merged ca output shifts by +1 for all ten waves (ca08a 2007 -> 2008 … ca25j 2024 -> 2025). The reference year is not lost: a new executable derived variableasset_reference_year(DV00,wave_values) carries it per respondent, and the recipe header documents the new semantics. ca recipe 1.2.0. - cv DV06/DV07/DV08 deleted: they materialized all-NA shadow duplicates of the BR01/BR02/BR03 flag columns. DV09 (
fieldwork_month) is re-expressed as an executable derivation (fieldwork_ymmod 100), so it carries real calendar months wherever a wave has a_mvariable instead of being all-NA everywhere; waves without_mstay NA pending an archive-metadata backfill. cv recipe 1.2.0. - cr BR20/BR21/BR30 (the 2019 scale breaks in religious attendance, prayer frequency, and afterlife belief) are re-expressed as executable
split_variablerules with explicitoutput_vars: each produces pre-2019 and post-2019 columns (attendance_pre2019_8pt/attendance_post2019_6pt, theprayer_*pair, andafterlife_pre2019_4pt/afterlife_post2019_3pt) so era-specific analysis never has to pool the raw stacked column across the break. cr recipe 1.3.0. - The audit’s nonconforming set drops from 10 to 7 (all tracked TODO items), and the pinned snapshot test now expects exactly those seven.
Stage 5a: the cf module repair, verified against the full archive
The Family and Household recipe was the one module whose execution layer failed in both directions (core missing-code rules inert, sentinel recodes scoped to every numeric column, string-map recodes that would null items if naively re-keyed, and a labelled policy that factorized monetary amounts). It is rebuilt on evidence: a new scanner (inst/scripts/verification/cf_scan.R) read all 17 archive waves with SPSS user-missing values preserved, and every rule scope below is verified against that scan (v1.4/cf_scan_results.json, 2026-07-11).
-
labelled_policyswitched fromto_factortoto_numeric, aligning cf with the other nine modules. Underto_factor, partially labelled variables (18 to 49 per wave, including euro amounts and paradata) became factors: numeric recodes skipped them andwrite_sav()re-coded levels 1..k, detaching output codes from the codebook. - HARM-005 (999) re-keyed to its three verified DK items 166/180/181: the label holds in all 17 waves and NO other column is 999-labelled anywhere, so the legitimate 999s on durations and euro amounts are provably untouched. HARM-006 (9999, cf11d onward) and HARM-007 (99999, cf08a-cf10c) re-keyed to the ten verified monetary items; cf12e lacks six of them (handled by if_absent), and cf08a’s suffix 008 carries no 99999 label (its Dutch label appears in cf09b/cf10c), exactly as the recipe’s defensive note said.
- HARM-001 (-9) rewritten from an inert module-wide sweep to evidence-scoped recode blocks: the imputation-flag trio 398/399/400 from cf10c (whose user-missing declarations are inconsistent across waves, so the recode is load-bearing), 535 from cf22o, and 554/555/556 in cf24q. HARM-004 (-8) scoped to its single verified item 535 (cf22o onward).
- HARM-002 and HARM-003 re-expressed as documentation: the evaluation-item label eras (boundaries verified at cf13f/cf14g and cf21n/cf22o) are translation artifacts on stable codes, and the school-denomination schemes keep their codebook codes (cf12e’s “Islamitic” spelling variant recorded). The old string-map value_recode forms would have coerced the items to NA had they ever executed.
- VAR-002 (fieldwork month) and VAR-004 (completion time; the F8-to-F10 widening lands exactly at cf20m) re-keyed and executing; VAR-003 retired, its cf11d type-inconsistency claim does not reproduce in any wave.
- End-to-end verification on real waves (cf08a, cf11d, cf24q): per-rule recode counts match the scan ground truth exactly (472 / 1,046 / 1,522 / 143 / 112 cells), the seven duration 999s and one 999-euro amount survive, zero sentinels remain on targets, fieldwork_ym carries the real months, and the output is flagged valid_for_analysis. The audit’s nonconforming set drops from 20 to 10, none of them cf.
Stage 4: per-module fixtures, provenance, and release pins
- Every bundled recipe now merges a synthetic multi-wave panel end to end in the test suite (
tests/testthat/test-stage4-fixtures.R). The fixture generator derives each module’s columns from its own recipe (rule and check suffixes, value sets implied by value_in_set/value_range checks, planted values for value_present checks, and structural-absence declarations honored as NAs), and the assertions are strict: no rule rolls back, no unimplemented action is hit, every declared flag column is non-degenerate in the OUTPUT, no check skips, and no error-severity check fails. - The fixtures immediately caught three real defects, now fixed: cp
V07’smax_waves: 16predated the cp25q onboarding (a full-panel respondent can legitimately appear in 17 waves, so the check would have failed on real data);value_absencecoerced forbidden values to numeric, so character codes (chCHK10’s forbidden wave id) collapsed to NA and mis-matched; and the uniqueness executor’s$lookups could be hijacked by partial matching (cdCHK11’sscope_wavekey matched$scopeand became the key column). Check-payload lookups now use exact indexing, anduniquenessaccepts the singularvariablekey. -
merge_liss_module()gains provenance and an explicit quality verdict: the returned object carriesprovenance(package, recipe, and schema versions, recipe file md5, per-input md5 hashes, release decisions, strictness, timestamp) andvalid_for_analysis(TRUE only when no error-severity check failed or was unevaluable and all release pins matched); both are written into the text report. - Release-selection transparency: when several files match one wave, the ranking decision (selected, ignored, rule) is recorded in provenance and the report. A
wave_indexentry may pinexpected_release: "1.1p"; a violated pin warns, clearsvalid_for_analysis, and aborts understrict = TRUE. - New
overwriteargument onmerge_liss_module():FALSErefuses to clobber an existing merged output (defaultTRUEpreserves behavior).
Stage 3b: per-action payload validation and the corpus audit
action_vocabulary.yml now carries a machine-readable payload specification per action (reads: the keys the executor consults; annotations: action-specific documentation keys), and validate_recipe() checks each rule’s keys against its own action instead of one global recognized-key set. A key read by one action no longer passes silently on an action that ignores it; documentary and pending actions (note_no_op, pending_spec, stub, and the note_only/flag_only/flag_absence no-ops) are exempt by design. The registry loads in .onLoad() alongside the vocabulary.
The sharper scan surfaced and fixed another round of silently inert or misfiring rules: cr BR70-BR79 presence flags never materialized (flag_name on structural_na, which reads flag_column); cd D01’s nohouse_encr drop was inert (singular variable); cs DR01_open_text dropped nothing (a stems list the drop executor does not read; now an executable pattern); cs checks V02/V03/V04/V08 passed vacuously (scopes written as stem_NNN, which double-prefixes in find_col); cr HR30 lowercased every labelled column instead of its nine targets (suffixes instead of scope); cv VR03/VR07 were re-expressed as documentation (stacking already covers both); cv VR04/VR08 and ch V06 were re-keyed and now execute (V06’s Dutch label fix is live).
New export audit_liss_recipes(): a corpus conformance report (per-module rule conformance, check classification, wave-metadata consistency, pattern canonicality) suitable for CI gating. The regression suite pins the audit snapshot: exactly 20 known nonconforming rules remain, every one mapped to the planned cf repair stage, the cv party pipeline, or a TODO item; any new nonconforming rule fails the suite. cd’s five free-text check types were re-typed to executable primitives and two to documentary types; corpus totals are now 68 executable, 31 documentary, 0 skipped.
Stage 3a: executable validation checks
The recipe validation layer now evaluates most of what the recipes declare. Before this change, 70 of the 99 checks across the ten bundled recipes named types the runner did not implement and silently reported SKIP; five recipes had no executable recipe-level checks at all.
- Check-type aliases normalize recipe-specific names onto the canonical executors: the uniqueness family (
unique_key,no_duplicate_ids,unique_per_wave,assert_identifier), the value-absence family (none_equal,sentinel_absence,no_residual_sentinels,assert_no_values,value_absence_check,value_restrictionas the allowed-waves complement), the range family (value_in_range,assert_range), the NA-rate family (na_rate_above,na_rate_below,not_missing), and the structural-missingness family (structural_absence,all_na,structural_na_count,missingness_checkincluding the expected-present-plus-NA-elsewhere shape). - New primitives:
value_in_set(shared or per-variable allowed sets,allow_na),value_present(a code must occur, per wave),row_count(per-wave or total bounds),per_wave_mean(mean bounds per wave, the DK-spike detector), andwave_countgains anexpectedexact form.value_absenceaccepts block payloads (targets/checkslists with per-block waves, columns, and codes) and exclusion lists. - Declared diagnostics that need distributional tests or inputs unavailable at merge time (distribution comparisons, panel consistency, presence matrices, and similar) are classified as documentary: they report
DOCinstead ofSKIPand are counted separately (n_doc). - A
severity: errorcheck whose type cannot be evaluated is escalated: a loud warning always, and understrict = TRUEthe merge aborts before writing outputs (an error-level check must be executable). The validation summary and return value now carryerror_skips. - The action vocabulary is refreshed from the installed
action_vocabulary.ymlin.onLoad(); the install-time probe that could resolve a previously installed version is gone. Stale vocabulary statuses corrected (crosswalk, conditional_label_swap, label_to_string, derive_combined_party, and transform are implemented; fix_label’s canonical keys areold_fragment/new_fragment). - Recipe corrections required to activate checks: cs
V06re-typed from an empty string tovalue_absence(its block payload now executes), and csV07’s expected wave count corrected from the stale 17 to 18 (cs25r was onboarded in 1.3.0).
Stage 2: recipe re-keys and pattern normalization
Every bundled recipe now says what the engine executes. Rule intents are unchanged; payload keys the engine never read are re-expressed in canonical form, so seven flag/period columns that used to materialize all-NA (or under a wrong fallback name) are now populated, one inert recode executes, and one unfulfilled promise is documented instead of implied. Regression tests in tests/testthat/test-stage2-recipes.R sweep every boundary flag rule in every bundled recipe and assert non-degeneracy against synthetic data.
- cr:
BR01(redesign_2019, now 0/1 via waves_pre/waves_post),BR02(instrument_phase),BR10(religion_coding_era),BR60(eval_wording_period), andBR80(fieldwork_season) re-keyed to the executableeras/phases/flag_columnforms.fieldwork_ymis declared under expected_presence again now that the stage-1 engine fix makes the declaration safe. - ch:
B12re-keyed (s020_wording_period now materializes under its own name instead of an all-NAB12_period);B06crosswalk_rename realigned toold_suffix/new_suffixwithharmonized_name: h_premium_period, so the ch08b premium-period item really coalesces with suffix 261; theD02drop of the historical strayh_column is retired to documentation. - ci:
A-09realigned (nine VUT/prepensioen pairs) with explicit harmonized columnsh_q363throughh_q371;DR03retired likewise. - cp:
A9_paradata_anchor_periodsre-keyed;paradata_anchor_regimeis now populated with its three anchor regimes. - ca:
B02(ca25j_redesign) andB03(ca20g_largeneg_review) re-keyed to executable forms;H04’spreserve_originalpromise is removed from the description because no copy mechanism exists yet. - cw:
HR03_pension_datesre-expressed as a wave-scopedvalue_recode(cw25r: 1 to 2023, 2 to 2024); previously the rule never executed and cw25r categorical codes pooled raw against calendar years. - All
file_patterns normalized to the extension-agnostic{wave_id}_*form (85 entries across cd, cf, ci, cp, cs were.csv-suffixed or bare and only loaded through the fallback matcher); the cd10c supersession pin (cd10c_EN_1.1p*) is preserved. -
recipe_versionbumped on all nine touched recipes;CANONICAL_SCHEMA.md’s crosswalk-alias known-limitation section replaced with the resolution note. Merged output gains new additive columns (populated flags and harmonized series); no existing column changes meaning.
Stage 1: engine correctness fixes
No recipe, schema, or API changes; four behaviors of the merge engine are corrected and pinned by regression tests in tests/testthat/test-stage1-fixes.R.
-
fieldwork_ymis now derived from the{wave_id}_mconvention BEFORE the expected-presence check runs. Previously, declaringfieldwork_ymunderglobal.expected_presence(as nine of the ten bundled recipes do) created an all-NA placeholder first, which suppressed the derivation entirely, so merged outputs carried an all-NAfieldwork_ymand a misleading “created as NA” warning for every wave that actually had_mdata. The cr recipe’s workaround note is retired in the stage-2 recipe pass. - Cross-wave type harmonization is factor-safe. When the same column is a factor in one wave and numeric in another (registry archetype TD-02, and any module under
labelled_policy: to_factor), the column is now coerced throughas.character()with a warning naming the affected columns. Previously the factor side was passed toas.numeric(), which stacks level indices (1..k) against real codes in the same column. - The residual user-missing sweep honors recipe
excludevetoes for q/Q-prefixed column names, mirroringfind_col()’s full candidate ladder. Previously a veto written against a q-form name was silently ignored and the swept cells could lose documented substantive codes. -
value_recodeleaves an audit-log entry when a target suffix does not resolve (:TARGET_ABSENT, honoringif_absent) or resolves to a non-numeric column (:SKIPPED_non_numeric). Previously such targets vanished from the JSONL log entirely. - Internal: dead variable removed from the
wave_countvalidation check.
lissr 1.3.2
Completes the party-scheme taxonomy: all five schemes catalogued from archive value labels, three of the four vacant registry slots named, and the taxonomy schema generalized to carry the pre-registry schemes.
Full catalogue (taxonomy 2.0.0)
- Schemes 1 (cv08a054/058), 2 (cv13f207/209), and 3 (cv18j307/308) join schemes 4 and 5, catalogued verbatim from SPSS value labels with zero data rows read. The schema generalizes accordingly: suffix-agnostic field names (
code_actual/code_hypo), per-schemehypo_offsetand special codes,verified_wavesper scheme, and separate actual and hypothetical party tables for scheme 1, whose two items carry different party sets (LPF and Een NL only in the actual vote, Trots op Nederland only in the hypothetical). - The registry finding is now scoped precisely: schemes 1 and 2 renumber wholesale between schemes (CDA is 1, then 5, then 3) and must never be pooled across schemes; the persistent registry begins at scheme 3, whose 2017 list is carried code-for-code into schemes 4 and 5.
- Three vacant scheme-4 slots are named with in-archive evidence: 5 GroenLinks, 7 PvdA, 10 50PLUS (reoccupied in scheme 5). Code 19 remains pending, expected to surface in a cv22n harvest together with the late scheme-3 code sets (the 2021 election falls inside the scheme-3 span), both recorded as open items in the taxonomy.
- Tests extended: per-scheme offsets, scheme-1 set divergence, the renumbering anchors, registry stability from scheme 3 onward, and named retired codes cross-checked against their evidence scheme.
lissr 1.3.1
First cut of the party-scheme taxonomy, and a factual correction to the cv26r scheme note.
Party-scheme taxonomy
- New reference file
inst/recipes/taxonomies/cv_party_scheme.yml(the externalization the cv recipe’s meta has pointed at since the schema was written). Schemes 4 (cv24p, cv25q) and 5 (cv26r) are fully catalogued from the SPSS value labels of the 307/308 vote items, verbatim, including the special codes and the 307-to-308 offset relation; schemes 1 to 3 are declared with their waves and marked pending. Engine consumption remains future work. - New tests pin the taxonomy to the recipe’s per-wave scheme declarations and turn the catalogued registry invariants into regressions: unique codes per scheme,
code_308 = code_307 + 1for every entry, and identity of every code shared between schemes 4 and 5.
Correction
- The 1.3.0 note on cv26r said the post-election mapping was “not code-comparable with scheme 4”. Cataloguing the value labels shows the opposite mechanism: codes form a stable party registry. Every code shared by schemes 4 and 5 denotes the same party; the delta is exactly 50PLUS entering at its historical slot (307:10, 308:11) and NSC leaving (307:21, 308:22), with retired codes left unassigned rather than reused. The wave entry and the meta note now state this; the scheme-5 declaration itself stands, since the id names the code set a wave can contain.
lissr 1.3.0
Wave onboarding release: the four 2025/2026 waves enter the recipes, and the onboarding diff is repaired so future onboardings rest on real comparisons.
New waves onboarded
-
ch25r,cp25q,cs25r, andcv26rare added to their module recipes:wave_indexentries,covered_waves, and every wave-scoped rule whose condition persists into the new wave (46 recipe edits in total). The extensions were decided by a structural and label-level review against the predecessor wave of each module. -
ch25r: 29 new suffixes 278-306 (a two-week symptom-frequency battery and a stress-domain block), no removals; reference-year labels updated. -
cp25qandcs25r: structurally and label-identical to their predecessors. -
cv26r: political-item churn after the 29 October 2025 parliamentary elections (added 221/256 for the returning 50PLUS and 353-355 for new politician items; removed 306, 341, 343-345). The wave declaresparty_scheme: 5because the post-election ballot mapping is not code-comparable with scheme 4; the value-label catalogue for the pending party-scheme taxonomy is noted in the wave entry.
Onboarding diff repaired
-
onboard_new_wave()step 3 previously reconstructed the previous wave’s variable names from the new wave’s own names, so it always reported zero additions and hardcoded zero removals. It now locates the actual previous wave file through the recipe’sfile_pattern(with the engine’s fallback and release-version disambiguation), reads it, and reports the real bidirectional suffix diff. A newprev_fileargument overrides the automatic resolution; when no file can be found the report carriesdiff_skipped = TRUEinstead of a silent empty diff. - The new-wave reader now goes through the engine’s
read_wave_file(), so SPSS user-defined missing codes stay visible to the step-6 sentinel scan.
lissr 1.2.1
Correctness patch: three guarantees the package documents are now enforced by the engine. No recipe format or output format changes.
Engine hardening
- Recipe
conditionexpressions no longer pass througheval(parse())withenclos = baseenv(). Conditions are parsed, their syntax tree is checked against a whitelist grammar (column references, literals, comparison and logical operators,%in%,is.na(),c(), unary sign), and evaluation is enclosed in a sandbox whose parent is the empty environment. A condition such assystem("...")is rejected before evaluation and the check reports “condition not evaluable”; recipes are declarative data again.liss_select()wave input is parsed by a regex-guarded expander accepting the1:5and1,3,7forms instead of being evaluated. - Rule execution is atomic on failure. Every executor snapshots the frame and the log before its dispatch; on error the rule rolls back to the snapshot and contributes exactly one
ERROR:entry, so the JSONL log now means a rule either fully applied or did nothing. The<<-accumulation in the error handlers is retired. -
merge_liss_panel()asserts per module that inputs are unique on the join keys (naming the module and the duplicate count), performs every join withrelationship = "one-to-one", attaches shared columns with a left join so metadata can never add rows, and coalescesshared_cols(by defaultnohouse_encr) across modules in list order instead of taking them from the first module only, warning when modules disagree on a key.
Tests
- A synthetic end-to-end module merge now runs everywhere including CI: two generated
.savwaves pass through a snapshot recode, a boundary flag, a derived variable, and uniqueness plus condition-gated na_rate checks, with assertions on the merged frame, the JSONL log, the text report, and the summary artifact. - Kernel unit tests cover
crosswalk_map,crosswalk_map_scheme,crosswalk_coverage, anddv_aggregate. - The recipe-load test covers all ten bundled recipes (previously eight, omitting ca and cr) and fails on any warning outside the known unrecognized-rule-key class.
- Regression tests pin the restricted condition grammar, the wave-input parser, atomic rollback, the duplicate-key guard, and the shared-column coalescing.
lissr 1.2.0
Feature release: a rule-driven income-cleaning framework for merged LISS data. Every behavior below is exercised by the regression tests in tests/testthat/test-clean-income.R and by a seeded end-to-end smoke run (inst/scripts/verification/income_cleaning_smoke.R). The ruleset is in inst/cleaning/income_cleaning_rules.yml.
Income cleaning
- New
liss_clean_income()detects, evaluates, and corrects implausible household-income values under a declarative YAML ruleset (inst/cleaning/income_cleaning_rules.yml, schema 1.0.0) of 24 rules: six preparation rules (P01-P06), eleven detectors (D01-D11), six correction-candidate generators (C01-C06), and one finalizer (F01). Rules dispatch by action, evaluate in ruleset order, and every rule carries a description, rationale, parameters, and literature references that the generated report reproduces. - Full decision transparency. Original values are preserved unchanged in
<target>_observed; every modified cell is marked in<target>_clean_statuswith its final action and rule; and every decision, applied or proposed, lands in an 18-column typed ledger with the responsible rule, the evidence, the admissible candidate set with sources, the anchor, the valid range, and a plain-language justification.liss_cleaning_report()renders the methodology directly from the ruleset plus a decision appendix, and writes the ledger as CSV and an engine-shaped JSONL audit log. - Three modes.
correctapplies corrections;flagis a true dry run whose<target>_proposedcolumn is identical to whatcorrectwould write while the data remain untouched;na_onlyvoids detected cells without imputing. Re-running on already-cleaned data aborts (the*_observedcolumn acts as a double-cleaning guard). - Researcher control.
income_cap,min_income,disable,enable_only, per-ruleparams, avariablesmapping, and custom ruleset files are honored and recorded in the run metadata and the report, so a reviewer can reproduce any configured run from its report alone. - Seeded smoke evidence: on 400 synthetic households (2,168 rows) with 158 planted errors across seven families, recovery is 100 percent in every family (65/65 decimal shifts, 33/33 extra zeros, 9/9 cap blowouts, 22/22 personal-income echoes, 11/11 tiny junk values, 11/11 sign flips, 7/7 residual sentinels), 0 of 2,010 clean cells are falsely modified, 83 percent of scale corrections land within 10 percent of the true value (median relative error 3.8 percent, at the simulation’s noise floor), and the run completes in about half a second.
- The modified-z detector (D10) gained a
min_relative_deviationgate (default 0.3) after the smoke run exposed that the ungated legacy criterion rewrote 12.09 percent of clean cells in tight households, where a tiny MAD inflates the z-score of ordinary variation. The gate removes every false positive with recall unchanged; the tight-household case is pinned in the regression suite. -
liss_equivalise_income()converts household income to a per-equivalent-adult scale (weighted_sqrt, matching the source pipelines’stand_incformula, plusoecd_modifiedandsqrt). - New exports:
liss_clean_income(),liss_cleaning_ruleset(),validate_cleaning_ruleset(),liss_cleaning_report(), andliss_equivalise_income(), with print and summary methods for run results and rulesets. A new vignette,income-cleaning, walks the workflow. - No new dependencies. Temporal smoothing uses a native weighted moving average (equivalent to
imputeTS::na_mawith linear weighting, including window widening), and the numeric kernels are base R.
Corrections to the source cleaning logic
The framework supersedes the income-cleaning blocks of the two analysis scripts it was distilled from. Eleven behaviors were deliberately changed, each pinned by a regression test, among them: the donor pool no longer offers the flagged row as its own donor; the power-of-ten kernel returns a full-length vector so zeros and negatives cannot desynchronize magnitudes from rows; the target variable resolves by explicit name and alias instead of a net|brut pattern match that could capture personal-income columns; blanket abs() on the target became the ledgered sign-rectification rule P03; bound violations rank by deviation ratio rather than first index; households process in wave order rather than file order; and residual SPSS user-missing codes are swept by the declared metadata (P06) rather than trusted to upstream reads.
Tests
- 44 new test blocks in
tests/testthat/test-clean-income.R: kernel units against hand-computed values, one fixture household per detector, ledger invariants, mode contracts, determinism and row-order independence, override paths, alias and fallback resolution, background attachment, report artifacts, and equivalisation. - Full suite in installed-package context: 74 test blocks, 208 passing expectations, 0 failures, 6 skips (5 empirical gates behind
LISSR_VERIFICATION_DIR, 1 CRAN skip).
lissr 1.1.0
Correctness release. Every fix below was verified against real LISS Panel files; regression checks are recorded in tests/testthat/test-engine-regressions.R.
Merge engine
- Value recodes now use snapshot semantics.
value_recode,recode_to_na, andrecodemasks are built against the column as it stood when the rule started, so overlapping maps can no longer chain. On real data the old sequential loop misclassified 86 of 2,992 answered respondents (2.9 percent) in cr08a and 312 of 2,222 (14.0 percent) in cr14g once the religion crosswalks were made to run; the snapshot engine reproduces the verified target distributions exactly. -
recode_to_naaccepts therecode:alias for its sentinel map and honors wave-scopedexclude:blocks (skip cells whose suffix and wave both match). This activates the cv module’s HR01 through HR04 sentinel rules, which previously validated but never executed. - Superseded-release protection. When several primary files match one wave’s
file_pattern, the engine ranks release versions parsed from the file names, keeps the highest, and warns; unrankable candidates abort. Auxiliary files declared viaaux_filesresolve independently of the primary pattern and must be disjoint from the primary file on the id variable; any shared respondent id aborts. A duplicate-id gate runs on every wave regardless. Previously all pattern matches were stacked, which duplicated all 3,626 cd10c respondents when both the 1.0p and the superseding 1.1p release were on disk, and resurrected a field the 1.1p release had redacted. -
read_wave_filereads by extension from a whitelist (.sav, .zsav, .dta, .csv) and aborts on anything else instead of parsing it as CSV. SPSS files are read withuser_na = TRUE, so declared DK/refusal codes reach the recipes as values instead of being silently converted to NA at read. - Value labels and user-missing declarations round-trip under
labelled_policy: to_numeric. Metadata is stashed per wave at read time and restored at write time where provably safe (identical metadata across waves, every observed value accounted for); columns that cannot be restored pass through a per-wave residual sweep that converts codes their own wave declared user-missing to NA, honoring recipeexcludeblocks as a veto. Outputs regain their value labels (75 labelled columns in the cv verification merge) and no DK or refusal code can leak into the output as a substantive value. - Rules that resolve zero target columns write a
NO_TARGETSentry to the JSONL log, so mis-keyed or mis-scoped rules are visible in the audit trail. - Validation checks that name an unimplemented type report
SKIPwithpassed = NAinstead of PASS; summaries reportn_pass,n_fail, andn_skip. Newuniquenesscheck family (aliasesassert_unique,n_duplicates). - New
strictargument onmerge_liss_module()andmerge_liss_modules(): failed checks of severityerrorabort before any output is written. DefaultFALSEpreserves 1.0.0 behavior. - Two-factor authentication returns
NULLon failure instead of a half-initialized session, soliss_login()can no longer cache an unauthenticated session as logged in.
Recipes
- cr: HR10, HR11, HR12, HR20, HR21 scoped with
suffixes(the singularsuffixkey was not read, so the entire three-era religion harmonization silently never ran). All 40 crosswalk code assignments were verified against the wave value labels and codebooks. HR12’s instrument description corrected to 14 values plus -9. - cd: cd10c loads the superseding 1.1p release only; the stacking rule is retired and the overlap claim corrected (both releases contain the identical 3,626 respondents; 1.1p removes the redacted open-text dwelling-location item cd10c059). CHK06 and CHK07 re-pointed at the executable
uniquenesscheck. - cp: the A1 DK recodes scope with
suffixes: ["010", "011", "019"](theitemskey was not read, so the recode fell back to every numeric column, destroying legitimate duration paradata). - cs: A1_dk_recode rewritten as an executable
value_recode(999 to -9) on the verified DK suffixes 001, 002, and 283 for the pre-cs20m waves. - cv: schema_version 1.1.0; HR01 through HR04 now execute through the engine’s
recodealias andexcludesupport (no rule changes needed beyond comments). - Revised recipes carry
recipe_version: 1.1.0.
lissr 1.0.0
First public release.
- Recipe-driven merge engine. Longitudinal LISS waves are merged from declarative YAML recipes that conform to the canonical schema (
CANONICAL_SCHEMA.md, schema version 1.0.0). A recipe captures every merge-relevant decision for a module: wave file patterns, variable harmonization, boundary handling, comparability contracts, and validation checks. - Controlled action vocabulary with fail-fast validation. Recipes are validated before any merge runs via
validate_recipe()(also called byload_recipe()andmerge_liss_module()); unknown actions and malformed rules are rejected up front. - Authoring-time check for unrecognized rule keys.
validate_recipe()emits a non-fatal warning listing any rule-level key that the merge engine neither consults nor sanctions as documentation, so mis-named keys are surfaced at load time rather than ignored silently. The check is warning-only; every recipe still loads and merges unchanged. The recognized set and the documentation allow-list are both documented inCANONICAL_SCHEMA.md. - Audit-grade JSONL logging, with a per-run summary artifact.
- Ten built-in module recipes: Assets (ca), Housing (cd), Family and Household (cf), Health (ch), Economic Integration (ci), Personality (cp), Religion and Ethnicity (cr), Culture and Sports (cs), Politics and Values (cv), and Work and Schooling (cw).
- Authentication against the LISS Data Archive with two-factor verification; credentials stored via the system keyring.
- Interactive browse, select, and download workflow (
liss_modules(),liss_wave_matrix(),liss_select(),liss_download()). - New-wave onboarding via
onboard_new_wave()to extend an existing recipe to a newly released wave.