Carried from
.planning/codebase/CONCERNS.mdin the 2026-07 GSD migration and only lightly reviewed. Some entries are stale (e.g.bee-sidebar.tswas deleted in v3.9; the Lambda surface was retired — see ADR 0007). The durable, still-live items worth tracking are thedbt-core==1.10.1exact pin (a 1.10.20 macro-parserKeyError:'javascript'regression), the FORMAT-CSV GeoJSON emission workaround, the 84-row county-boundaryST_Withinnondeterminism, and the static-hosting scaling ceiling. A full reconciliation against current code is tracked as a beads issue.
Analysis Date: 2026-05-13
CR-01: Collector filtering by iNat username — RESOLVED (silently)
- Original issue (Phase 67):
bee-filter-controls.tsusedobserverfield whilefilter.tsCollectorEntrydefinedhost_inat_login. Filtering by iNat username silently failed. - Current state (verified 2026-05-13): both interfaces use
host_inat_login(src/bee-filter-controls.ts:20,src/filter.ts:8), and the WHERE clause is wired up atsrc/filter.ts:245–255(host_inat_login IN (...)joined OR withrecordedBy IN (...)in a single collector clause). The bug was fixed without a tracking commit, likely as a side effect of the v2.7 Unified Occurrence Model schema unification. - Status: Resolved — clearing from STATE.md "Blockers/Concerns" during this codebase remap.
- Outstanding: no dedicated unit test for the iNat-login filter path. See Test Coverage Gaps below.
dbt-core 1.10.20 regression — macro parser keyerror
- Issue:
dbt-core==1.10.*was pinned to exact version1.10.1indata/dbt/run.sh(line 30, 33). Version 1.10.20 introduced a macro-parser regression (KeyError: 'javascript') that breaks all dbt commands. The exact-version pin (==1.10.1) is a workaround, not a solution. - Files:
data/dbt/run.sh - Impact: dbt commands fail immediately if the 1.10.20 regression is present. Upgrading dbt-core to any newer 1.10.x version is blocked until the upstream dbt-core issue is fixed or a 1.11.x release is available.
- Fix approach: Monitor dbt-core releases for a fix in 1.10.21+. If fixed, remove the exact-version pin and test a
1.10.*specifier again. If not fixed, evaluate upgrading to dbt-core 1.11.x when available (requires testing the dbt-duckdb adapter against the newer dbt-core). - Status: Active, pins build dependencies
- Recorded: Commit fffc496 ("fix(083): pin dbt-core to ==1.10.1 exactly"); documented in dbt-spike-findings.md §What Was Awkward
FORMAT CSV GeoJSON emission fragile
- Issue: Writing a bare GeoJSON FeatureCollection from DuckDB requires a workaround:
COPY ... TO '...' (FORMAT CSV, DELIMITER '', QUOTE '', HEADER false)with an explicit::VARCHARcast. This bypasses DuckDB's JSON machinery and is underdocumented, making it fragile across DuckDB version changes. - Files:
data/dbt/models/marts/(emit_feature_collection macro, exact path in Phase 83/84) - Impact: GeoJSON output files (
counties.geojson,ecoregions.geojson) are generated correctly today but depend on a DuckDB-specific workaround. Any DuckDB version bump may change CSV or JSON handling, silently breaking the macro. - Fix approach: Before any dbt full-rewrite cutover, re-evaluate the GDAL-driver alternative (
FORMAT GDAL, DRIVER 'GeoJSON') to see if it can matchexport.pyoutput byte-for-byte. If not, stabilize the FORMAT CSV path with an integration test against multiple DuckDB versions and explicit documentation. - Status: Deferred to v3.4 rewrite
- Recorded: dbt-spike-findings.md §Open Trade-Offs and §Verdict condition #3; documented as prerequisite for cutover
84-row county boundary nondeterminism
- Symptoms: County assignment varies non-deterministically for specimens on county boundaries (Grant/Benton, Grant/Kittitas, Chelan/King, Garfield/Whitman). Same specimen may be assigned to different counties across pipeline runs.
- Files:
data/dbt/models/intermediate/int_county_base.sql(dbt port);data/export.py(original Python implementation) - Trigger:
ST_Within(geometry, polygon)returnsTruefor two adjacent polygons simultaneously when the specimen is exactly on a shared polygon edge. Thewith_countyLEFT JOIN matches multiple counties; no deduplication occurs before the fallback path selects one. JOIN ordering (which varies between Python DuckDB and dbt DuckDB, or between runs within the same implementation) determines the winner. - Workaround: Use
SELECT MIN(county)orSELECT FIRST(county)in the fallback path to enforce deterministic selection. Current code does not guarantee determinism. - Root cause: This is a semantic divergence in BOTH implementations (export.py and dbt port), not a regression introduced by the dbt rewrite. The nondeterminism exists in the original Python pipeline as well. Fixing it requires changing the spatial-join logic, not the dbt port.
- Impact: 84 rows (out of 47,883) exhibit boundary assignment variance. This is approximately 0.175% of the dataset. For users, it means a rare specimen on a county boundary may appear in different counties on different export runs.
- Status: Active, semantic divergence requiring investigation
- Recorded: dbt-spike-findings.md §DIFF-02; classified as "semantic divergence to investigate"; documented as prerequisite for cutover (Verdict condition #2)
Stale schema.yml comment — RESOLVED
- Original issue:
data/dbt/models/staging/schema.ymllines 23–26 carried a pre-research prediction comment that did not match the actual TEST-01 outcome. - Current state (verified 2026-05-13): comment correctly reads "TEST-01 outcome (awkward-fit): not_null FAILS with 1 null id; unique PASSES (10,846 rows, all distinct)." Fixed in commit
f50b9b2. - Status: Resolved.
None detected. The codebase is a static-hosted web application with no server-side auth, secrets, or user-submitted data processing. AWS credentials are handled via GitHub OIDC (no stored secrets). Environment configuration uses .env files (gitignored).
SQLite query latency on large result sets
- Problem: Frontend queries via
wa-sqlite+hyparquet(src/filter.tsqueryTablePage, queryAllFiltered) may slow down as the specimen table grows. Current dataset: 47,883 rows. Frontend uses pagination (100 rows per page) to avoid loading entire result set into memory. - Files:
src/filter.ts(lines 126–189) - Cause: wa-sqlite is single-threaded and runs in the browser. Large WHERE clause evaluation (multi-county, multi-month filters) must be computed in JavaScript's SQLite runtime. As the dataset scales, row-by-row filtering becomes proportionally slower.
- Improvement path: Monitor query execution time in production. If latency becomes noticeable (>500ms for typical page load), consider: (1) pre-filtering the SQLite data during the Eleventy build to only include WA-relevant rows, (2) pre-aggregating filter options by geographic region to reduce the default WHERE clause, (3) moving to DuckDB-WASM for in-browser SQL (deferred due to page-weight concerns; documented in dbt-spike-findings.md §Frontend Impact).
- Status: Potential bottleneck; not currently a blocker
- Recorded: Not formally documented in planning; noted as a forward-path consideration
Collector autocomplete and filtering
- Files:
src/bee-filter-controls.ts(autocomplete + token construction),src/filter.ts(lines 245–255, collector OR clause) - Why fragile: The collector path depends on consistent field naming (
recordedByvshost_inat_login) across three places —CollectorEntry(filter.ts:5),CollectorToken(bee-filter-controls.ts:20), and the WHERE clause builder. Adding or renaming fields requires coordinated changes; unit-test coverage for the iNat-login filter branch is thin. - Safe modification: When changing collector-related fields, update all three sites together and add a unit test in
src/tests/filter.test.ts(or sibling) covering: (1) iNat login autocomplete suggestions, (2) WHERE clause generation when bothrecordedByandhost_inat_loginare selected, (3) URL-state roundtrip. - Test coverage: Phase 82 UAT confirmed
recordedBy-based collector filtering works. Thehost_inat_loginbranch (filter.ts:251–255) is reachable but lacks a dedicated unit test.
dbt model contract enforcement
- Files:
data/dbt/models/marts/schema.yml(33-column contract withenforced: true) - Why fragile: The contract is the only mechanism pre-empting schema drift before parquet is written. If the contract is accidentally removed or set to
enforced: false, then schema drift detection moves toscripts/validate-schema.mjs(post-export CI gate). This two-layer defense is intentional, but the dbt-side contract is the primary developer-facing gate and must remain enforced during development iterations. - Safe modification: Any change to the
occurrencesmart SQL (data/dbt/models/marts/occurrences.sql) must be accompanied by re-runningbash data/dbt/run.sh buildand confirming the contract passes. If adding/renaming columns, update the contract inschema.ymlfirst, then update the mart SQL. - Test coverage: Phase 084 §TEST-02 documented contract drift detection with a live example (renaming
countytocounty_renamedpre-empts the build). The contract mechanism is proven.
Spatial joins and boundary geometry (84-row nondeterminism)
- Files:
data/dbt/models/intermediate/int_county_base.sqlanddata/export.py - Why fragile: The
ST_Withinspatial join on county boundaries is non-deterministic by design (DuckDB + PostGIS semantics). Both implementations exhibit the same 84-row variance. Any modification to the spatial-join query (e.g., adding a fallback for ecoregion assignment) must be tested against known boundary-case specimens to ensure determinism is maintained or intentionally enforced. - Safe modification: Before any changes to spatial joins, (1) identify the 84 boundary-case specimens in the live data, (2) run the query multiple times and confirm assignment is consistent, (3) if still non-deterministic, add an explicit
MIN(county)deduplication step, (4) verify the change does not affect ecoregion assignment (currently 0 divergences). - Test coverage: Phase 084 §DIFF-02 confirms the 84-row variance with a repeatable pytest harness (
data/tests/test_dbt_diff.py).
Static site hosting ceiling
- Current capacity: 47,883 specimen records in SQLite; ~300 KB parquet + ~50 KB species.json + ~10 KB boundaries GeoJSON = ~1.5 MB total data payload per user session.
- Limit: Browser memory (wa-sqlite in-memory database) is typically 4-8 GB per tab. At current data sizes, no limit is hit. However, if WA expands to multi-state (v3.4+ roadmap goal), the dataset could grow to 500K+ specimens. wa-sqlite would still fit in memory, but SQL query latency would become a concern (see Performance Bottlenecks).
- Scaling path: (1) Continue static hosting by pre-filtering data per state during the Eleventy build, (2) Move to a server-backed API when per-state datasets exceed ~100 KB parquet, (3) Adopt DuckDB-WASM for in-browser analytics on multi-state datasets (v3.4+ stretch goal, currently deferred).
- Status: Not a current blocker; flagged for v3.4 planning
- Recorded: CLAUDE.md "Constraints"; multi-state expansion noted in project memory; documented as "Future scaling path" in dbt-spike-findings.md §Frontend Impact
dbt-duckdb adapter version sensitivity
- Risk:
dbt-duckdb==1.10.1is pinned exactly due to dbt-core 1.10.20 regression (see Tech Debt section). The adapter depends on dbt-core, and newer versions of dbt-core may break the adapter. There is no explicit test of the adapter across multiple dbt-core versions. - Impact: Upgrading dbt-core is risky and requires full regression testing of the data pipeline. Staying on 1.10.1 means missing security patches and features in newer dbt releases.
- Migration plan: When dbt-core 1.11.x is released, test the full pipeline against
dbt-core==1.11.xanddbt-duckdb==1.11.xbefore merging. Add a CI step to verify dbt --version matches the run.sh pin, preventing accidental version drift. - Status: Active, pins development velocity
- Recorded: dbt-spike-findings.md §What Was Awkward; documented as an operational risk for v3.4 rewrite
None identified. The v3.3 dbt Spike spike confirmed that the export.py → occurrences.parquet pipeline can be faithfully ported to dbt-duckdb with correct outputs and test coverage. The v3.4 full-rewrite milestone has five explicit prerequisites (documented in dbt-spike-findings.md §Prerequisites), but no missing critical functionality.
Collector filtering (iNat username) untested
- What's not tested: The
host_inat_loginbranch of the collector OR clause atsrc/filter.ts:251–255. The filter is wired (CR-01 silently resolved) but has no dedicated unit test. - Files:
src/filter.tsbuildFilterSQL()function;src/tests/filter.test.ts(add coverage here) - Risk: Future refactors of the collector clause could silently break the iNat-login branch without surfacing in CI. Cases worth covering: a specimen with both
recordedByandhost_inat_loginpopulated; multiple iNat logins selected; single-quote escaping in usernames (already handled atfilter.ts:252). - Priority: Low — the branch is small and the risk of silent breakage is bounded, but worth picking up alongside the next collector-feature change.
dbt iNat key-set validation gap
- What's not tested: Direct assertion that the set of
host_observation_id(iNat sample IDs) is identical between sandbox and public outputs. Phase 084 §DIFF-01 explicitly requires this; current test coverage is indirect (row count + ecdysis_id anti-join arithmetically constrains iNat rows). - Files:
data/tests/test_dbt_diff.py(missingtest_occurrences_inat_key_set_matches) - Risk: If a dbt model change silently drops iNat sample rows, the row count would still match (due to a compensating drop in ecdysis rows, unlikely but possible), and the ecdysis_id key-set would match. The iNat key-set test would catch this.
- Priority: Low for v3.3 (spike); Medium for v3.4 (full rewrite cutover)
- Recorded: Phase 084 VERIFICATION.md human-verification item #2; resolved in commit f50b9b2 (test added)
GeoJSON whitespace formatting regression test
- What's not tested: Ensuring
counties.geojsonandecoregions.geojsonformatting does not regress when GeoJSON generation changes (e.g., switching from FORMAT CSV workaround to FORMAT GDAL). Phase 084 §DIFF-03 noted GeoJSON whitespace as "neutral/cosmetic," but a test confirming byte-stable output would catch accidental regressions. - Files:
data/tests/test_dbt_diff.py(could add atest_geojson_whitespace_stable()check) - Risk: If the FORMAT CSV macro is swapped for GDAL output, the GeoJSON structure might change (adding
crs,id,bboxfields), breaking the frontend's assumptions. - Priority: Low for v3.3; Medium for v3.4 (GeoJSON format evaluation)
speicmenLayer typo in bee-map.ts
- What it is:
src/bee-map.ts:70declares a private placeholderprivate speicmenLayer: unknown;(the property name has a typo: "speicmen" instead of "specimen").@ts-ignoreon the line above marks it as intentionally unused until a separate specimen layer is wired up. - Status: Intentionally deferred — documented in CLAUDE.md "Constraints": "
speicmenLayertypo inbee-map.tsis intentionally deferred — do not fix incidentally." - Why deferred: The property is a placeholder for a future map layer. The typo is preserved across commits as a marker — fixing it incidentally would obscure the trail of when the placeholder was introduced and why. (Originally placed during the OL era; the map is now Mapbox GL JS, but the placeholder semantics carry over.)
- Fix approach: Rename when the specimen layer is actually implemented, not as a cleanup task.
- Impact: None at runtime — the property is unused.
Boundary edge gaps and overlaps
- What it is: Adjacent region polygons (counties/ecoregions) have small gaps and overlaps where the right edge of one polygon is approximated differently from the left edge of the next. This is a GeoJSON simplification artifact.
- Files:
public/data/counties.geojson,public/data/ecoregions.geojson - Status: Deferred (TODO item
.planning/todos/pending/boundary-edge-gaps.md, priority: low) - Impact: Visual artifacts on the map where boundaries do not perfectly align. Does not affect data correctness.
- Fix approach: Use TopoJSON-aware simplification or pre-process boundaries with
topojson-serverto enforce topological consistency. This is a GIS/cartography task, not a data pipeline issue. - Recorded: Commit 193a57b (Phase 73 verification)
Cluster blob selection visual feedback
- What it is: When a user clicks a cluster blob on the map, selection state is captured (sidebar opens,
_selectedOccIdspopulated), but the cluster itself shows no visual indication (yellow selection ring does not render on clustered features). - Files:
src/bee-map.ts(cluster layer filtering and styling) - Status: Deferred (TODO item
.planning/todos/pending/cluster-selection-visual-feedback.md, priority: medium) - Impact: UX dead-zone; clicking a cluster registers selection but provides no map-side feedback.
- Fix approach: Explore auto-zoom on cluster click (common pattern) or add a halo overlay layer. See TODO file for detailed options and tradeoffs.
- Recorded: Phase 071 design decision (promoteId conflict with cluster auto-IDs); TODO updated in commit 02dff62 (Phase 73)
The dbt-spike-findings.md §Prerequisites section documents five explicit prerequisites that must be satisfied before a full-rewrite milestone proceeds. These are not bugs or tech debt, but structural decisions and validation steps required for safe cutover:
-
Test coverage: Every invariant enforced by
validate-schema.mjsand_apply_migrations()must be re-expressed as dbt tests or contracts. Gaps:relationshipstest (cross-type key casting),_apply_migrationsDDL equivalent. -
Schema decisions: The
samples.parquetvsoccurrences.parquetshape must be finalized (one-file fold vs two-mart split). This unblocks contract enforcement and frontend schema updates. -
Ingestion-vs-transform boundaries: dlt-style raw-schema ingestion must be decoupled from dbt transform-only operations. The seam design (dlt writes raw, dbt reads as
source()) must be tested for freshness and incremental load behavior. -
Parallel-run / orchestration: The
data/nightly.shcron must be redesigned to handle dbt exit codes and awkward-fit test failures. Incremental materialization behavior on dbt-duckdb + external materializations must be tested. -
Frontend impact: The output schema of
occurrences.parquetmust remain stable, or frontend consumers (wa-sqlite + hyparquet) must be updated in coordination. The dbt contract andvalidate-schema.mjspost-export gate must both remain active as complementary defenses.
Lambda CDK artifacts exist but are inactive: Infrastructure-as-code definitions for AWS Lambda exist in infra/, but the active execution path is data/nightly.sh running on a maderas machine (nightly cron). The Lambda artifacts are stale and should not be used for deployments.
Schema validation gate: scripts/validate-schema.mjs runs before every CI build as a parquet schema gate. This is the last line of defense before production deployment to CloudFront.
Concerns audit: 2026-05-13