Skip to contents

This article explains how the PIP data pipeline is orchestrated end to end: which functions run in which order, which package owns each step, and how to run a full session. Code in this article is illustrative and does not execute when the article is built — running the pipeline requires a configured working release, network access to Datalibweb, and write access to PIP storage.

For the internal mechanics of DLW acquisition and validation, see the companion article Validating Data; for survey cleaning, deflation, and logging, see Processing Data functions.

Current pipeline entry points

The pipeline lifecycle uses three explicit entry points:

Order Wrapper Package Developer scope Purpose
1 update_aux_measures() pipaux Auxiliary data engineer Refresh auxiliary data (CPI, PPP, population, GDP, PCE, PFW)
2 pipdata_dlw_process() pipdata Survey ingestion engineer Download new Datalibweb (DLW) survey files, validate them, update the DLW inventory
3 pd_run_pipeline() pipdata Survey processing engineer Incrementally run clean, metadata, and deflate from completed validation state

update_aux_measures() lives in the {pipaux} package and is not detailed in this {pipdata} article beyond this table — it is a prerequisite step that should run before Step 2 so that the latest auxiliary data is available.

pipdata_dlw_process() is the supported DLW acquisition/validation entry point. pd_run_pipeline() is the top-level incremental entry point for the existing durable processing stages. The older pd_process_data() and pd_deflate_pipeline() wrappers remain independently callable and keep their master-inventory returns.

Important: pd_process_data() cleans and attaches metadata to survey data — it does not deflate welfare. Deflation is a separate, currently manual step; see Deflation is a separate step below.

How to run: an end-to-end session

0. Configure the working release

Start by checking for package updates, then configure the working release that all three wrappers, and the functions that consume their output, read from:

# Check for updates to the PIP package ecosystem before running the pipeline
metapip::update_pip_packages()

release  <- "20260401"
identity <- "TEST"

pipfun::setup_working_release(release, identity, verbose = FALSE)

1. Refresh auxiliary data (pipaux)

# Owned by pipaux; see the pipaux package documentation for details.
pipaux::update_aux_measures()

2. Acquire and validate DLW data (pipdata)

dlw_result <- pipdata::pipdata_dlw_process(
  inv_gmd_list      = "dlw_gmd_inv",
  get_dlw_data      = TRUE,
  validate_dlw_data = TRUE,
  check_missing     = TRUE,
  release           = release,
  identity          = identity
)

The wrapper requires explicit nonempty release and identity values. Their formal NULL defaults are missing-required-value sentinels, not operational defaults; identity is restricted to "PROD", "INT", or "TEST".

This acquires five active modules and validates all seven recognized module mappings, producing completed validation inventory (gmd_valid_inv) for Step 3. The assigned result is a plain aggregate. Inspect dlw_result$outcome (success, partial, failed, or no_work) before deciding whether a script should continue. Disabled or dependency-blocked nested stages use not_run.

Runtime stage failures after setup are represented in nested compact failure tables and can yield partial or failed results. Invalid arguments, release setup/precondition errors, explicit cancellation, and interrupts still escape. Under automation a missing acquisition inventory returns failure rather than opening a menu; validate-only execution never prompts. See Validating Data for the exact result and persistence contracts.

3. Run clean, metadata, and deflate incrementally (pipdata)

pipeline_result <- pd_run_pipeline(verbose = FALSE)

pd_run_pipeline() loads the completed validation inventory internally (via pipload::load_gmd_valid_inv()) when inv is not supplied. It accepts current valid and invalid available rows under existing cleaning policy, filters recognized legacy blank/unavailable retry rows before planning, then runs only stale or forced clean:<survey_id>, metadata:<pip_id>, and deflate:<pip_id> nodes. It returns a compact pipdata_pipeline_result; it does not return the master inventory. See Processing Data functions for the internal mechanics.

Current nodes are cached. Stale or forced nodes are runnable. Failed prerequisites make descendants blocked/skipped. force_surveys adds selected survey or PIP chains without suppressing independent staleness. Unknown legacy provenance requires explicit bootstrap = TRUE; use bootstrap_entities for a restrictive canary.

The C2 manifest and exact Stamp receipts are the currentness authority. A Colombia 2018 CPI change runs only matching Colombia 2018 metadata and deflate nodes, while another Colombia year and unrelated country/year remain cached. There is no persisted run cursor. After failure, call pd_run_pipeline() again; it replans from the last valid manifest and exact Stamp facts.

4. Build a consolidated log report

log_report(
  path      = file.path("log_reports", "log_report.md"),
  overwrite = TRUE
)

log_report() loads "pipdata_log" internally and summarizes DLW acquisition, DLW validation, survey cleaning, deflation, and structured failures. DLW stage sections use each latest attempt and exact completion metadata, with confined fallback for older logs. DLW discriminators are excluded from generic type and country tables to avoid double counting.

Architecture and compatibility

Opt-in M2 recipe authority (TEST only)

pd_run_pipeline(recipes = ...) is a separate, bounded route. The default recipes = NULL retains the staged pipeline above and its existing return. The opt-in route does not call its selectors or C2 currentness manifest. It passes complete requests and all supplied outputs to public stamp planning and rebuild APIs; stamp alone decides run, skip and blocked work and revalidates at execution time. No production stage adapters are activated.

# Illustrative only: choose an already approved, disposable E fixture root.
root <- "E:/approved-m2-fixture"
stamp::st_init(root, alias = "m2-fixture")
recipes <- list(
  context = list(release = "m2-example", identity = "TEST", root = root),
  nodes = list(list(
    path = "value.rds", parents = list(),
    calculation = function(path, parents) list(x = value),
    helpers = list(), constants = list(value = 42L),
    semantic_schema = NULL, semantic = list(), format = "rds"
  ))
)
feedback <- pd_run_pipeline(recipes = recipes, verbose = FALSE)

Each node requires exactly the fields shown; optional audit is descriptive only and optional review names a governing failure outcome_id. Parents are an unnamed list of list(path = ..., version_id = ...). Each pin is an exact nonempty character ID, never a numeric position or "latest"/"current" selector. Only a selected upstream recipe may use version_id = NULL; stamp resolves it within the selected first-build graph. Multiple roots and diamonds are supported; cycles are rejected. nodes = list() returns the typed empty feedback without acquiring a lease. Nondefault force/bootstrap/checkpoint controls and inv cannot be combined with recipes. Formats are exactly rds, qs2, and fst; the callback cannot override the requested format.

The callback has exactly (path, parents) formals, consumes supplied exact pins, and returns exactly one named x, optionally metadata, format, code, and code_label. A named NULL value is different from omitting x; physical formats must support the value. Declared semantic values are supplied as metadata and any callback-supplied semantic fields must agree, preserving types and missing, NULL and NA distinctions.

Pure calculation code, named ordinary-R helper closures and disjoint named immutable constants are copied into a locked environment with a base parent. SHA-256 component descriptors cover the actual copied bodies/formals and values, not a manually supplied digest or package-version label. Reordering declarations does not invalidate; helper/constant/body edits do. An audit-only edit does not. This deliberately narrow trusted callback contract rejects mutable captures, missing constant values, primitive components, hidden dependencies, dynamic lookup/dispatch and direct side effects. It is not a sandbox or support for arbitrary legacy closures, and does not discover undeclared dependencies. Guarded reads require the effective declared parent path/pin pair and explicit format. Ambient locale/timezone conversions and unsupported S3 generics are rejected; guarded base operations reject classed operands rather than silently dispatching an unfingerprinted method. Explicitly unclass reviewed inputs before operations. The internal read/base/closure policy code is itself SHA-256 bound. Nested classed list elements are rejected too, including implicit method dispatch inside lengths(). Consumed producer exports and explicit arguments are checked before recipe preparation, lease or rebuild; a version label alone is insufficient. Nonempty recipes require the existing suggested package codetools. When it is unavailable, pipdata_engine_dependency_error gives an actionable approved-library requirement before proof or write-side work. No package is installed automatically and fingerprint proof is never bypassed. Availability-seam tests do not certify a dependency-minimal installation.

Feedback is one row per selected artifact: release_id, canonical path, stamp preview_decision, actual status/reason, version_id, request_id, receipt_id, outcome_id, failure_class, governing_failure_id, message, and actual attempted. Missing proof IDs are typed NA, not fabricated successes. Exact returned receipt IDs and same-pin pipload::pip_read(metadata_only = TRUE) must validate before successful feedback is returned. Retained-output work can return unchanged with a new receipt and the same output pin; an unchanged downstream artifact can consequently remain skipped.

Deterministic and unknown failures block descendants while independent nodes can continue. External failures are retried only by another explicit call. Signal classified calculation failures with rlang::abort(..., class = "stamp_deterministic_failure") or rlang::abort(..., class = "stamp_external_failure"); unclassified errors are unknown failures. A review supplies list(outcome_id = feedback$governing_failure_id[[i]]) on the relevant node after an actual parent/component change. The failure ID is recovered from committed exact outcome history, not a process-local cache or error text; it remains usable after repeated blocked calls and a fresh process.

A dedicated lease under the selected root is scoped to release/TEST/root, never to a code fingerprint, and does not publish a legacy currentness manifest. Frozen proof, root, options and lease ownership are checked on every callback entry and exit, including errors. A failed integrity guard stops later callback attempts and raises pipdata_engine_integrity_error with actual feedback and original/secondary causes; cancellation still propagates as cancellation.

Verification for this slice is deliberately partial: only three selected test files and a built/installed public-API smoke run in an E-only disposable runtime. The ordinary test suite, production DLW/survey/deflation workflows, production SMB semantics, full concurrency and umbrella milestone closure are not certified.

Existing staged architecture

Each stage is intentionally isolated because a different engineer developed it independently, with its own auxiliary data, error handling, and logging:

  • update_aux_measures() (pipaux) manages the auxiliary-data lifecycle: dependency resolution, GitHub/Y-drive sync, and change detection.
  • pipdata_dlw_process() (pipdata) is responsible only for getting raw survey data into a validated state. It can continue validation after an acquisition failure when a trustworthy durable inventory remains, routes a custom inventory ID through both stages, and records summary/checkpoint facts. It does not clean or transform welfare data.
  • pd_process_data() (pipdata) consumes the validated inventory and the refreshed auxiliary data, and is responsible for the cleaning/metadata transformation. Internally, it iterates one survey at a time, so that a per-survey failure is caught, logged, and skipped without aborting the rest of the run.

The top-level executor composes these existing stage cores. Only clean, metadata, and deflate are durable cached nodes. Load, PFW merge, recode, auxiliary attachment, and save remain internal fingerprint components.

Each survey’s cleaning pass inside pd_process_data() follows: inv_dlw_load() (load raw survey) -> pd_cpfw_merge() (merge Price Framework metadata) -> pd_dlw_clean() (S3-dispatched cleaning) -> pd_aux_attr() (attach CPI/PPP/population/GDP/PCE metadata) -> save_pip_data() (write cleaned data and metadata to PIP storage). No deflation step is part of this chain.

Deflation is a separate step

pd_deflation()/deflation() deflates a single cleaned survey’s welfare values using CPI, PPP, and population auxiliary data. It is not called automatically by pd_process_data() — it must be run afterwards, per survey, once cleaned data exists in PIP storage:

# Mode A: pass a cleaned survey directly (aux auto-loaded from the master
# inventory when cpi/ppp/pop are NULL)
dt <- pipload::pip_read(id = "BOL_2022_EH_INC_ALL", alias = "pip")
bol_deflated <- pd_deflation(dt)

# Mode B: load the survey by id and deflate in one call
bol_deflated <- pd_deflation(pip_id = "BOL_2022_EH_INC_ALL")

{pipload} also provides load_pip_deflated_data(), a convenience wrapper around pd_deflation()’s Mode B: it locates a survey (by id_name or by filter arguments such as country_code/surveyid_year/module), loads it, and deflates it in one call — useful when you don’t already have the survey’s pip_id in hand:

bol_deflated <- pipload::load_pip_deflated_data(id_name = "BOL_2022_EH_INC_ALL")

# Or filter by country/year/module instead of a known id_name
bol_deflated <- pipload::load_pip_deflated_data(
  country_code  = "BOL",
  surveyid_year = 2022,
  module        = "ALL"
)

See Processing Data functions for details on both modes.

Production activation block

Local tests do not activate production. Production remains blocked until the signed target Windows/SMB fencing and immutable unique-rename evidence are complete.

Logging and reporting scope

log_report() summarizes the shared "pipdata_log" entries from pipdata_dlw_process() and pd_process_data(). It adds stage-aware warnings for DLW-only, pipeline-only, incomplete, and DLW no-op runs. Acquisition and validation are segmented independently at their latest attempt boundaries; dedicated DLW sections own those records. Auxiliary refresh logging from pipaux remains a separate follow-on scope.