Repository navigation
Run provenance log on persist schema: log + full config snapshot + input fingerprints #127
Description
Problem
<persist_schema>.streams, streams_habitat_<sp>, streams_access, streams_mapping_code and barriers accumulate rows across many runs but carry no provenance. Given a network in the database today you cannot answer:
- which config produced it (and what that config actually said at the time)
- which vintage of the DB primitives it was built from
- which link/fresh code wrote it
- when it was written, and whether neighbouring WSGs in the same schema came from a different run (mixed-vintage state)
lnk_pipeline_persist does DELETE-WHERE-WSG + INSERT, so re-runs cleanly overwrite a WSG — that's the rewrite-is-enough property the orchestrator depends on, but it leaves no audit trail.
This blocks scenario work directly. #236 proposes running the DV-as-BT override with and without and adding a watershed-group axis; those runs land in the same persist schema and are indistinguishable a week later.
What can change between two runs (inventory)
| # | Category | Recorded today |
|---|---|---|
| 1 | Config bundle — config.yaml (schema, break_order, gradient_classes, cluster, spawn_connected), rules.yaml, dimensions.csv, parameters_fresh.csv, 13 override CSVs |
git-tracked (89 files under inst/extdata/configs/); nothing in DB |
| 2 | DB primitives — fwa_stream_networks_sp, bcfishobs.observations, whse_fish.pscis_assessment_svw, cabd.dams, fresh.modelled_stream_crossings, fwa_lakes/wetlands/rivers_poly, fwa_obstacles_sp, fwa_stream_networks_channel_width, fwa_stream_networks_order_parent, public.wsg_outlet |
nothing — data-raw/snapshot_bcfp.sh loads them with no stamp |
| 3 | Code — link / fresh / crate versions + SHAs | nothing |
| 4 | Run args — aoi, schema, dams, cleanup_working, mapping_code, LNK_SCHEMA |
nothing |
| 5 | Cross-WSG accumulated state — which WSGs were already persisted when this one ran | nothing |
Two notes:
- (2) is the bigger hole.
config.yamlpins the bcfp CSVs (each carriesupstream_sha), but the bcfp-derived data in the database arrives viasnapshot_bcfp.shfrom objectstore/BCDC/s3 and is recorded nowhere. Results move with observation vintage and nothing explains why. - (5) is link-specific. Per RUNBOOK §5, a WSG's
;DAMtokens depend on which other WSGs were already in persist — same config, same code, same data, different run order gives different output. bcfp never hits this (province in one shot). If we log nothing else unique to link, log this.
Also: lnk_config_verify() exists (R/lnk_config_verify.R:67) but nothing calls it during a run. Config provenance is declared, not verified — so hash at run time rather than copying the provenance: block, which could record a lie.
Reference: how bcfishpass does it
bcfishpass.log (model_run_id PK, model_type, date_completed, model_version) with FK'd children that snapshot the actual parameter values, not pointers:
log_parameters_habitat_thresholds— per species:spawn_gradient_max, channel widths, mad,rear_lake_ha_minlog_parameters_habitat_method,log_wsg_linear_summary,log_wsg_crossing_summary,log_aw_linear_summary,log_wcrp_habitat_connectivity
Run 134 (v0.7.15-45-gf8db4b9, 2026-08-04) records BT at spawn_gradient_max 0.0549 in the database. The network stays interpretable after the config file moves on. That's the property we want.
Proposed solution
Rename from the original persist_log: "persist" is the phase that writes, not the thing described — the unit is a run. Noun-first matches link's existing tables (streams, barriers, streams_access) and mirrors bcfp's log / log_<noun>.
<schema>.log — one row per lnk_pipeline_run() call
| column | type | notes |
|---|---|---|
run_id |
bigserial PK | |
watershed_group_code |
text NOT NULL | |
date_start |
timestamptz NOT NULL | |
date_end |
timestamptz | NULL until the run completes → partial runs visible |
run_label |
text | groups a campaign across hosts/WSGs |
host |
text | LNK_HOST_ALIAS else Sys.info()[["nodename"]] |
config_name |
text | cfg$name |
config_hash |
text | sha256 over the resolved bundle, computed at run time → joins log_parameters_fresh / log_dimensions |
link_version / link_sha / link_dirty |
text / text / bool | dirty tree makes a SHA a lie — record the flag |
fresh_version / fresh_sha / fresh_dirty |
text / text / bool | |
crate_version |
text | |
arg_dams, arg_mapping_code, arg_cleanup_working |
boolean | |
schema_persist |
text | resolved target (after LNK_SCHEMA override) |
species |
text[] | species actually resolved for the AOI |
wsg_upstream |
text[] | WSGs already persisted at date_start — captures (5) |
bcfp_model_run_id / bcfp_model_version |
int / text | from bcfishpass.log when reachable, else NULL |
notes |
text | free-form |
INSERT-only. Latest per WSG = SELECT DISTINCT ON (watershed_group_code) … ORDER BY watershed_group_code, date_start DESC.
<schema>.log_parameters_fresh and <schema>.log_dimensions — full rows
Full contents of parameters_fresh.csv and dimensions.csv keyed on (config_hash, species_code), columns mirroring the CSVs — bcfp's log_parameters_habitat_thresholds pattern. Stored once per distinct config_hash, not per run, so a provincial pass adds one set.
Generate DDL + INSERT projection from a single cols_* named vector (per the R conventions in CLAUDE.md) so column drift can't desync the two sites — the failure mode #204 describes.
This is what makes a network self-describing: observation_species = BT;DV at threshold 1 with no date floor is recorded in the database, so a scenario run is identifiable after the config changes.
<schema>.log_input — DB primitive fingerprints
One row per primitive per run: run_id, table_name, row_count, fingerprint, loaded_at (NULL until snapshot_bcfp.sh stamps).
Fingerprint at run time (row count plus a cheap content digest — e.g. max(observation_date) for observations) so this works before snapshot_bcfp.sh learns to stamp. Teaching the snapshot script to write real load provenance is the follow-up that fills loaded_at / source SHA.
Acceptance
- After a run,
<schema>.loghas one row withdate_start/date_endpopulated andwsg_upstreamlisting WSGs already persisted. -
log_parameters_fresh/log_dimensionshold the full CSV contents for that run'sconfig_hash; a second run on the same config adds no new rows. - Changing one value in
parameters_fresh.csvyields a newconfig_hashand a new parameter set — two networks in the same schema are distinguishable. -
log_inputrecords row counts for every primitive in the inventory above. - A killed run leaves
date_endNULL (partial state is visible, not silent). - Editing a tracked config file without committing sets
link_dirty = true.
Related
- Research: DV as BT access evidence — regional scope + multi-rule observation overrides #236 — DV as BT access evidence; scenario runs need this to be tellable apart
- run_provincial_parity.R cache key doesn't capture code/config version (silent stale-mix risk) #110 — parity cache key doesn't capture code/config version (same class of risk)
- lnk_config_audit + crt_schema_audit: verify bundled CSVs against schema, provenance, and peer DB #86 —
lnk_config_audit: verify bundled CSVs against schema/provenance (lnk_config_verify()exists, is uncalled) - Persist+consolidate must be host-/species-count-agnostic: persist_init blind to species-column-set drift #204 — persist_init blind to species-column-set drift (why DDL+INSERT share one
cols_*source) - Single-WSG downstream-state guard: fail loud when downstream dams aren't persisted #227 — reproducible
public.wsg_outletbuilder (a primitive with no provenance at all)
Activity
[-]Per-WSG provenance log on persistent schema (lnk_pipeline_persist + persist_log table)[/-][+]Run provenance log on persist schema: log + full config snapshot + input fingerprints[/+]Correct the network attribution for the published floodplain delineation
Merge main (v0.45.0, #127 run provenance) into #227 branch