Skip to content

Build the frame before st_sf() sees it, so the reporting columns survive a tibble - #36

Merged
NewGraphEnvironment merged 6 commits into
mainfrom
35-fly-footprint-drops-footprint-basis-and
Aug 30, 2026
Merged

Build the frame before st_sf() sees it, so the reporting columns survive a tibble#36
NewGraphEnvironment merged 6 commits into
mainfrom
35-fly-footprint-drops-footprint-basis-and

Conversation

@NewGraphEnvironment

Copy link
Copy Markdown
Owner

What

fly_footprint() silently dropped footprint_basis, footprint_terrain, height_agl and dem_coverage whenever its input carried the tbl_df class. bcdata::collect() returns exactly that class, so every caller querying WHSE_IMAGERY_AND_BASE_MAPS.AIMG_PHOTO_CENTROIDS_SP — the documented source for this package — lost the whole reporting surface 0.4.0 (#30) and 0.5.0 (#9) added.

Nothing errored. Geometry and every downstream number were correct; only the audit trail was gone, which is why two releases shipped with it.

Cause

sf::st_sf() builds its attribute frame with a branch that keeps only its first argument when that argument is a tibble:

else if (inherits(x[[1]], c("tbl_df", "tbl"))) x[[1]]

Every trailing named column is discarded. A general R trap rather than a fly quirk. The fix assigns the four columns onto the attribute frame first, so they are inside x[[1]] where they survive, and the caller keeps the class they passed in.

Why the suite could not see it

Every fixture in the package reads back as plain sf, data.frame — the bundled GeoPackage, and both synthesized fixtures, which are built by st_sf() on plain vectors. No case added along the existing axis could have found this. Seventh instance of the pattern in inst/notes/terrain-correction.md.

So the tests sweep the input-class axis instead — plain / tibble / grouped / bcdc_sf — asserting identical names, identical values and identical geometry across shapes. The tibble shape is read honestly via st_read(as_tibble = TRUE) rather than by overwriting class(), and the premise is asserted inline so a future sf change fails by naming the real cause.

Two further defects the reviews surfaced

Both reproduced before acting, both now guarded:

  • A colliding column was never overwritten. Where the input already carried footprint_basis, the old code appended a duplicate footprint_basis.1 and left the caller value under the documented name — so footprints$footprint_basis != "unknown_format", the filter the docs prescribe, read the caller's column while fly's own answer sat unread beside it. This hit plain sf callers too, who were never affected by the tibble bug.
  • Zero-row input returned logical columns. ifelse(logical(0), ...) is logical(0), so an empty result would not bind_rows() onto a populated one — exactly the per-AOI ledger this reporting surface exists for (stac_airphoto_bc#16).

Corrections to my own claims

Two things I wrote and the reviews falsified:

  • @return said "the input class is preserved". The class set survives; the order does not — st_transform() (not st_sf()) moves sf to the front, so bcdc_sf, sf, ... returns sf, bcdc_sf, .... An identical(class(out), class(in)) assertion would have passed on the three shapes I had and failed on the one caller the issue is about.
  • A NEWS line claimed the overwrite change made fly_footprint(fly_footprint(x)) idempotent. It does not: a plain 20-row sf comes back with 100 rows, silently, because st_coordinates() on polygons yields 5 rows per feature. Pre-existing and unchanged, but the sentence invited the round trip. Cut.

Verification

  • Suite 225 → 338, FAIL 0 | WARN 0 | SKIP 0
  • R CMD check Status: OK (the only gate on the Title rules — this repo has no R-CMD-check workflow)
  • Every new guard proven to fail against the real prior code from 8585fd5 — not a reconstruction — patching both asNamespace("fly") and the attached binding, with a proof-of-patch value printed before asserting
  • lintr unchanged; NAMESPACE byte-identical at 9 exports
  • End to end against the live catalogue, the case no fixture can reach:
rows: 1405
input class : bcdc_sf,sf,tbl_df,tbl,data.frame
output class: sf,bcdc_sf,tbl_df,tbl,data.frame
four columns present: TRUE
documented filter: 1254 of 1405        # 151 frames excluded that were invisible before

Also

Widens the DESCRIPTION Title and Description, which described roughly half the package — they predated fly_fetch(), fly_georef() and fly_bearing().

Released as v0.5.1. Not tagged on the branch/gh-pr-merge tags after merge, and a tag on a branch tip that review may amend moves silently.

Follow-up worth filing

The st_sf() trailing-column behaviour belongs in soul's conventions/code-check.md as the general rule — a constructor taking ... may discard trailing arguments depending on the class of the first one; pass a fully-built frame instead — with a sweep across the other NGE packages.

Closes #35
Closes #31

🤖 Generated with Claude Code

https://claude.ai/code/session_01GBKqedyBysV7hB4DuL98ZR

NewGraphEnvironment and others added 6 commits August 29, 2026 21:04
fly_footprint() loses footprint_basis, footprint_terrain, height_agl and
dem_coverage whenever its input carries tbl_df — which is what
bcdata::collect() returns, so the documented data source is exactly the
caller that cannot see them.

Every fixture in this package is plain `sf, data.frame`: the bundled
gpkg reads back that way, and both synthesized fixtures are built by
st_sf() on plain vectors. So no case added along the existing axis could
have found this. centroid_shapes() sweeps the axis instead, reading the
tibble honestly via st_read(as_tibble = TRUE) rather than overwriting
class(), and asserting that premise inline so a future sf change fails
by naming the real cause.

These fail on unmodified source. Refs #35

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GBKqedyBysV7hB4DuL98ZR
st_sf() keeps only its first argument when that argument is a tibble:
`else if (inherits(x[[1]], c("tbl_df","tbl"))) x[[1]]`. Every trailing
named column is discarded, so footprint_basis, footprint_terrain,
height_agl and dem_coverage reached no bcdata caller — which is to say
no caller using this package's own documented data source. Assigning
them onto the attribute frame puts them inside x[[1]], where they
survive, and leaves the caller's class alone.

Three things the reviews found, each reproduced before acting:

- 0-row input returned footprint_basis and footprint_terrain as
  logical, because ifelse(logical(0), ...) is logical(0). An empty
  result would not bind to a populated one, which is exactly the
  per-AOI ledger this reporting surface exists for. Seeded with
  as.character().
- An input already carrying footprint_basis got a duplicate
  footprint_basis.1 under the old code, and the filter the docs
  prescribe read the caller's column while fly's answer sat unread
  beside it. The computed value now wins.
- The class contract was stated too strongly. st_transform(), not
  st_sf(), moves sf to the front, so a bcdc_sf input returns
  sf, bcdc_sf, ... The set survives; the order does not.

Each new guard was checked by restoring the defect from 8585fd5 --
not a reconstruction -- patching both the namespace and the attached
binding, and printing a value only the broken code could produce.

225 -> 338 tests. Refs #35

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GBKqedyBysV7hB4DuL98ZR
Title and Description both predated fly_fetch(), fly_georef() and
fly_bearing(), so the pkgdown landing page and any generated citation
described roughly half of what is here. README and CLAUDE.md were
widened already; DESCRIPTION was the remainder.

R CMD check Status OK, which is the only gate on the title-case and
Description sentence rules -- this repo ships pkgdown.yaml and no
R-CMD-check workflow, so CI does not repeat it.

Closes #31

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GBKqedyBysV7hB4DuL98ZR
The headline feature of 0.5.0 was unreachable from the data source this
package documents, so the patch is the point of the release rather than
housekeeping.

NEWS records what was actually lost: geometry and every downstream number
were always correct, and only the audit trail went missing -- which is why
nothing errored and why the suite stayed green through two releases.

Verified end to end against the live catalogue: 1405 real centroids come
back as bcdc_sf, sf, tbl_df, ..., all four columns arrive, and the
documented footprint_basis filter excludes 151 frames that were invisible
before.

No tag here. /gh-pr-merge does the tagging after merge; a tag on a branch
tip that review may still amend moves silently.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GBKqedyBysV7hB4DuL98ZR
@NewGraphEnvironment
NewGraphEnvironment merged commit 603677d into main Aug 30, 2026
1 check passed
@NewGraphEnvironment
NewGraphEnvironment deleted the 35-fly-footprint-drops-footprint-basis-and branch August 30, 2026 04:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fly_footprint() drops footprint_basis and friends when input is a tibble (as bcdata returns) DESCRIPTION Title predates fetch and georeferencing

1 participant