Skip to content

Clear the fallow gate on native-product-pages - #1184

Open
busbyk wants to merge 6 commits into
native-product-pagesfrom
fallow-remediation
Open

Clear the fallow gate on native-product-pages#1184
busbyk wants to merge 6 commits into
native-product-pagesfrom
fallow-remediation

Conversation

@busbyk

@busbyk busbyk commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Description

fallow is the only red job on the native-product-pagesmain integration PR (#1130), and it is wired into the pre-commit hook, so it has been blocking every commit on this branch — both #1176 and #1183 went in with --no-verify. This clears it.

This is remediation of pre-existing debt spread across issues 01–05. It is deliberately not scoped to any one issue, and nothing here was introduced by #1183.

Stacking: this branch is cut from issue-05b-native-danger-map, so until #1183 merges the diff below includes 05b's commits. It resolves to just the fallow work once #1183 lands. Ordering doesn't matter for CI — ci.yaml only triggers on PRs targeting main, so the fallow job never runs on a PR into native-product-pages; the result shows up on #1130.

Before / after

Measured with fallow audit --changed-since origin/main (166 changed files):

Axis Before After
Dead code introduced 37 0
Complexity introduced 15 0
Duplication introduced 6 0
fallow dead-code --baseline 39 issues, exit 1 exit 0
fallow dupes --baseline exit 0 exit 0
Audit verdict fail pass

The baselines are untouched — nothing here was accepted as debt by re-baselining.

Related Issues

Follows the remediation plan in .scratch/native-product-pages/handoffs/fallow-remediation-handoff.md. Unblocks #1130.

Key Changes

Declare the deliberate public surfaces (clears 23 of 39 dead-code findings)

model/forecast.ts exists so the model is the single import surface for consumers, so its re-exports have no consumers of their own and read as dead — transitively taking their sources in types/forecastSchemas.ts with them. Both are now declared in ignoreExports, the same treatment src/components/ui/** already gets.

model/forecast.ts cited "ADR 018" from the start, but that ADR had never been written — docs/decisions/ stopped at 017. It is written here (018-native-product-page-architecture.md), since it is the justification for the config change.

Delete what was genuinely dead

  • src/utilities/getUseNativeForecasts.ts — an entire unused file; every call site had already migrated to getNativeProductFlag.
  • The unconsumed getProductDataSource re-export in sources/index.ts (the symbol is still used inside the file).
  • The ProductListItem alias.

Suppress the z.nativeEnum false positives

Members of AvalancheProblemType, ProductStatus and ExternalMediaType reached only through z.nativeEnum read as unused because zod validates against member values, not by-name references. They are load-bearing — removing WindSlab would make zod reject every forecast carrying a wind-slab problem — so each carries a suppression plus a docblock explaining why. Same pattern as AvalancheCenterType in schemas.ts.

⚠️ fallow fix offers "remove this enum member" as an auto-fix for these. Taking it would break validation of real forecast data.

Waive the two dynamic-segment conflicts

forecast-archive and forecast-freshness hit the same false positive that three sibling routes already waive: fallow reads (payload)/api/[...slug] and api/[center] as one dynamic path, but route groups keep the trees separate.

Complexity: 15 → 0

Mostly by extracting the pure decisions out of components into tested modules — which drops the parent's branch count and covers the logic that moved:

  • mediaItem.ts — thumbnail/full URLs, YouTube id, caption, and the slide-kind decision
  • datePickerNavigation.ts — month windows, link targets, arrow stepping, the archive fetch
  • dangerRatingLayout.ts — day headings and the "no rating" test
  • zoneCardDanger.ts — the highest-danger-band lookup
  • parseArchiveWindowQuery in archiveDates.ts — the archive endpoint's parameter validation

The JSX-heavy components (DangerScale, AvalancheProblemCard, ForecastDatePicker, NativeForecastView, ZoneForecastCard, MediaLightbox) are split into named sections. MediaSlide moved to its own file so it could be tested without standing up the carousel.

Two incidental fixes fell out: NativeForecastView and ZoneForecastCard each carried their own copy of the highest-danger-band lookup — both now call zoneCardDanger.ts.

63 new tests, all behaviour-preserving refactors.

Duplication: 6 → 0

  • src/utilities/centerRoutePage.ts — the per-center route preamble the built-in pages shared verbatim: generateStaticParams, the metadata shape (title suffix, canonical, OpenGraph, dynamic OG image), the Args/PathArgs types, and the platform guard (assertCenterPlatform, which 9 routes hand-rolled).
  • src/components/ZoomPanSurface.tsx — the zoom/pan interaction the Gallery block's ZoomableImage and the forecast lightbox's ZoomablePhoto had duplicated.

Not addressed

The 2 circular dependencies through the Payload config barrel. They are rated warn, gate nothing, and are already down from 27 at the July baseline. The only change that actually clears them is injecting payload across ~26 call sites — I tried a lazy await import('@payload-config') first and fallow counts that as a graph edge too, so the cheap version doesn't work. Left as-is deliberately rather than churning hook plumbing for a non-gating finding.

How to test

pnpm fallow:audit --changed-since origin/main   # what CI runs — passes
pnpm fallow:audit --ci                          # what the pre-commit hook runs — passes
./node_modules/.bin/fallow dead-code --baseline fallow-baselines/dead-code.json; echo $?   # 0
./node_modules/.bin/fallow dupes     --baseline fallow-baselines/dupes.json;     echo $?   # 0
pnpm tsc && pnpm lint && pnpm test && pnpm drift:check

All green: 885 tests pass, and this commit went in without --no-verify for the first time on this branch.

Behaviourally this should be a no-op. Worth a click-through of the forecast pages, since that's where the refactoring landed: a zone forecast (danger ratings, problems, media lightbox, date picker), a dated archive view, the all-zones grid, and the Gallery block's lightbox.

Migration Explanation

None — no schema or migration changes.

Future enhancements / Questions

  • One behaviour change to flag: weather/forecast/page.tsx used limit: 0 (unlimited) in its generateStaticParams where the other nine routes used limit: 1000. The shared helper normalises to 1000. With four tenants this changes nothing, but it is a real difference and I'd rather name it than bury it.
  • Six complexity findings remain inherited (not gated): seed/index.ts:78 (CRAP 1722, 1317 lines) and middleware.ts:49 (1122) are the big ones. They pre-date this branch and are out of scope here.
  • The dead-code baseline still lists four symbols in types/schemas.ts that Native avalanche danger map on Mapbox (issue 05, Part B) #1183 deleted. Harmless, but the baselines are due a refresh once this branch merges — worth doing deliberately rather than as a side effect.

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

busbyk and others added 6 commits August 11, 2026 10:49
The danger map's data is a Product in the ADR-018 sense, so it goes through a
source adapter like forecast and warning already do. Issue 02 scoped the adapter
to those two; this adds the third.

- `model/mapLayer.ts` — the normalized zone model consumers depend on, never a
  v2 or v3 response. Features carry a top-level `id` (the numeric zone id), which
  `mapLayerFeatureSchema` had been dropping; Mapbox's `setFeatureState` keys on it.
- `mapLayerSourceV2.ts` + mappers, wired through `sources/config.ts` so v3 is
  reachable by the Control 2 env flag. Defaults to v2, matching every other product.
- `getMapLayer` takes an optional `day`, which the NAC API has always supported and
  we never passed. Cheap now, expensive to retrofit onto issue 09's date picker.
- `getForecastZoneDanger` moves from `nac.ts` to `dangerMap/mapLayer.ts` alongside
  it; the OG route follows the import.
- `zoneSlug.ts` resolves a zone's upstream link to its AvyWeb path, so the popup,
  the zone list and the click handler can't disagree about where a zone goes.

Also drops four `z.infer` aliases and one schema with no consumers anywhere.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Everything the map draws is decided here, in pure functions, and tested — the map
component only renders the result. The rules are where the bugs are: off-season
outranking a stale rating, an unrated zone ignoring the server's grey, naive
timestamps that are really UTC.

Parity target is the afp danger-map widget, not avy. NAC has built this twice and
the two disagree — off-season and no-rating colors are *swapped* between them,
opacity and popup copy differ. AvyWeb sits on the same sites as these embeds, so
matching the embed is what a reader actually notices.

`dangerMapSettings` reads the full `widget_config.danger_map` contract that
dashboard-v2 writes, rather than a subset: every tenant has set `saturation`, SAC
has `search: false`, and `allCenters` is present on SNFAC/SAC but absent on NWAC.
Two departures from the stored config, both recorded in the parity ledger:
saturation is not applied (no Mapbox consumer in the NAC stack applies it, and the
shared style is already near-grayscale), and the configured zoom is used minus one,
because those values were authored against the Google Maps build and Mapbox's 512px
tiles render the same number a level tighter.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The widget titles the legend "Avalanche Danger Scale" and makes that heading the
disclosure trigger. We had a "Danger Scale" heading plus a separate "Danger scale
definitions" toggle underneath — extra wording the widget doesn't have, and a
second control competing for the same job.

The heading and the color strip both move inside `<summary>` so the strip stays
visible when the definitions are collapsed. Each scale row gains an abbreviation
("Mod", "Cons", "Extr") for the narrow-screen strip, where the full names collide —
the same shortening the legacy legend does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Establishes Mapbox in web — this is the repo's first map dependency — as shared
infrastructure rather than danger-map-local, so issue 16's station map reuses it.
Style is the `avalanche-org` "AFP Custom" style already shared by afp-public-widgets
and dashboard-v2; the token is NAC's public `pk.` token, which both of those
hardcode as a fallback.

Rendering knowingly diverges from both existing NAC implementations. avy renders
one ShapeSource per zone and the widget does the same with `zone-source-${index}`;
this uses a single `geojson` source with data-driven paint, so hover and the
warning flash are `setFeatureState` + `feature-state` expressions rather than
`setPaintProperty` across per-zone layer ids. Zones arrive pre-styled from
`decorateZoneFeatures`, so the paint expressions just read computed properties and
there is no second copy of the precedence rules to drift.

The map is a client component that fetches on mount through `/api/[center]/danger-map`,
not props from the statically generated home page. The host page renders on a
one-hour window and a map painted from hour-old ratings is exactly the staleness the
widget avoided by refetching every page load. Routing the fetch through our own
endpoint keeps the adapter, the zod validation and the styling rules server-side.

The warning flash is rebuilt on requestAnimationFrame at the widget's ~20fps rather
than its 50ms setInterval, so it pauses with the tab instead of burning a timer.

A Mapbox map is a `<canvas>`, so `ZoneList` renders the same zones as a visually
hidden list of links — otherwise today's avalanche danger would be unreachable by
keyboard or screen reader.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds `dangerMap` to the native product flags and swaps the legacy danger-map widget
for the native one on the home page. Ships dark: the flag is false for every tenant,
so untick and the widget comes back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`fallow` has been the only red CI job on the integration PR (#1130), and it is
wired into the pre-commit hook, so it has been blocking every commit on this
branch — 05a and 05b both went in with --no-verify. This is remediation of
pre-existing debt across issues 01-05, not a fix for any single issue.

Both gates now exit 0, and the audit reports no issues in 166 changed files.

Tell the tool about deliberate public surfaces
  `model/forecast.ts` exists so the model is the single import surface for
  consumers, so its re-exports have no consumers of their own and read as dead,
  transitively taking their sources in `types/forecastSchemas.ts` with them.
  Both are declared in `ignoreExports`, the same treatment `components/ui/**`
  already gets. This clears 23 of 39 dead-code findings on its own.

  ADR 018 was cited by `model/forecast.ts` but had never actually been written;
  it is written here, since it is the justification for that config.

Delete what was genuinely dead
  `getUseNativeForecasts.ts` (every call site had migrated to
  `getNativeProductFlag`), the unconsumed `getProductDataSource` re-export in
  `sources/index.ts`, and the `ProductListItem` alias.

Suppress the z.nativeEnum false positives
  Members of `AvalancheProblemType`, `ProductStatus` and `ExternalMediaType`
  reached only through `z.nativeEnum` look unused because zod validates against
  member *values*. They are load-bearing — dropping `WindSlab` would make zod
  reject every forecast carrying a wind-slab problem — so they carry per-member
  suppressions with a docblock, matching `AvalancheCenterType` in schemas.ts.

Waive the two dynamic-segment conflicts
  Same false positive three sibling routes already waive: route groups keep
  `(payload)/api/[...slug]` and `api/[center]` separate.

Bring the 15 gated complexity findings to zero
  Mostly by extracting the pure decisions out of components into tested modules,
  which drops the parent's branch count and covers the logic that moved:
  `mediaItem.ts`, `datePickerNavigation.ts`, `dangerRatingLayout.ts`,
  `zoneCardDanger.ts`, plus `parseArchiveWindowQuery` in `archiveDates.ts`.
  The JSX-heavy components (DangerScale, AvalancheProblemCard, the date picker,
  NativeForecastView) are split into named sections. 63 new tests.

  `NativeForecastView` and `ZoneForecastCard` each had their own copy of the
  highest-danger-band lookup; both now use `zoneCardDanger.ts`.

Collapse the 6 gated duplication groups
  `centerRoutePage.ts` holds the per-center route preamble the built-in pages
  shared verbatim — static params, metadata shape, and the platform guard.
  `ZoomPanSurface` holds the zoom/pan interaction the Gallery block and the
  forecast lightbox had duplicated.

Not addressed: the 2 circular dependencies through the Payload config barrel.
They are `warn`, gate nothing, and are down from 27 at the July baseline. The
only fix that actually clears them is injecting `payload` across ~26 call sites
— fallow counts a lazy `await import()` as a graph edge too, so that shortcut
does not work. Left as-is deliberately.

The baselines are untouched: nothing here was accepted as debt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Preview deployment: https://fallow-remediation.preview.avy-fx.org

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.

1 participant