Skip to content

The financials rename broke lint:admin-zones โ€‹

  • Status: gate green, 2026-08-27. npm run lint:admin-zones exits 0, and npm run validate -- --scope lint passes all 16 checks.
  • Cause: the apps/admin/src/admin/billing/ to apps/admin/src/admin/financials/ rename in 6f21f04a, which this project's financials consolidation drove.
  • Scope: apps/admin/src/admin/financials/ and tooling/scripts/admin-zones-baseline.json only.

What actually broke? โ€‹

The ratchet keys on file path, so a rename reads as twelve brand new errors. โ€‹

  • tooling/scripts/lint.admin-zones.mjs compares a per-file, per-rule ERROR count against admin-zones-baseline.json, whose keys are repo-relative paths. Move a file and its grandfathered count no longer matches anything, so every pre-existing violation is measured against a baseline of zero and fails.
  • Six baseline keys pointed at apps/admin/src/admin/billing/, a directory that no longer exists. Their twelve grandfathered errors resurfaced as new ones.

Only this ratchet was missed. Every other baseline was clean. โ€‹

  • Checked all seven: admin-zones, cta-classes, docs-format, no-em-dash, openapi-sync, secrets, story-coverage. Every path-shaped key in each was resolved against the working tree.
  • admin-zones-baseline.json was the only file with keys pointing at paths that do not exist. The em dash baseline had already been remapped by the session that did the rename.

What happened to each of the twelve? โ€‹

#FileRuleCountVerdictWhy
1financials/BillingReports.jsxreact-hooks/set-state-in-effect1RemappedByte-identical move (zero-line diff in the rename commit), and the fix is a fetch-lifecycle restructure that changes when the spinner appears
2financials/Financials.jsxreact-hooks/set-state-in-effect2RemappedThe rename touched only names and labels; both effects are verbatim from Billing.jsx. Restructuring the main page's data lifecycle is the riskiest of the eight
3financials/InfraCosts.jsxreact-hooks/set-state-in-effect1FixedGenuinely new code written today, so it never had a baseline entry to remap. See below
4financials/components/LineItemsTable.jsxreact-hooks/set-state-in-effect1RemappedByte-identical move, and another session has uncommitted work in this file
5 to 8financials/components/LineItemsTable.jsxeqeqeq4RemappedAll four are av == null / bv == null in a sort comparator. Loose is deliberate: it catches undefined too, so === would be a behaviour change, not a lint fix
9financials/components/PeriodPicker.jsxreact-hooks/set-state-in-effect1RemappedByte-identical move. The effect syncs the custom From/To inputs when the popover reopens, which is genuine derived state; the fix is a restructure with visible behaviour
10financials/vendors/VendorPageShell.jsxreact-hooks/set-state-in-effect1RemappedByte-identical move. Same fetch-lifecycle shape as #1
11financials/vendors/sections/VendorLineItemsSection.jsxreact-hooks/set-state-in-effect1RemappedByte-identical move. Same fetch-lifecycle shape as #1

A remap preserves the count verbatim. It never re-snapshots. โ€‹

  • Each of the six keys was renamed in place with its rule counts untouched, so the burn-down list is exactly as long as it was this morning. Nothing was forgiven.
  • --update-baseline was deliberately not run. A blind re-snapshot would have swallowed the InfraCosts violation along with the eleven carry-overs, which is the failure mode the ratchet exists to prevent.

What was fixed in InfraCosts? โ€‹

The error now clears on SUCCESS, not before the request. โ€‹

  • fetchCosts opened with setHealthError(null) and later setOtpError(null). Both moved to sit beside their setData call in the success path.
  • That is better behaviour independent of lint: clearing up front blanked the message the moment the page Refresh button started a retry, then flashed it back when the retry also failed. Holding it means an error stays put until something actually replaces it.

The fetch also moved inside the effect, because clearing on success alone did not satisfy the rule. โ€‹

  • react-hooks/set-state-in-effect flags a call out to a hook-level callback that sets state anywhere in its body. It does not trace the await boundary, so moving the first setState after the first await left the error exactly where it was. Verified against a throwaway probe file: useEffect(() => { cb() }) fails whether or not cb is async, while an async function declared inside the effect and called there passes.
  • fetchCosts had no caller other than that one effect, and it was useCallback(..., []), so its identity never changed. Deps [fetchCosts, refreshToken] reduce to [refreshToken] with no change in when the effect runs. useCallback became unused and was dropped from the import; the React import is untouched.

How was it verified? โ€‹

  • npm run lint:admin-zones: OK: no new errors in apps/admin zones (55 grandfathered, burn-down in admin-zones-baseline.json).
  • npm run validate -- --scope lint: 16 passed, 0 failed.
  • npx vitest run --root apps/admin src/admin/financials: 3 files, 15 tests, all passing.
  • InfraCosts has no test of its own. The change is confined to one component with a single caller (Financials.jsx renders it on the Costs tab), and it is a pure error-timing change on an already-linted file.

What is still on the burn-down list? โ€‹

  • Eleven of these twelve. They are real defects, not style nits, and they stayed visible in the baseline precisely so they can be burned down deliberately rather than forgotten.
  • The five set-state-in-effect fetch-lifecycle ones (#1, #2, #10, #11) share one shape: setLoading(true); setError(null) at the top of an effect that then fetches. They are worth doing as one pass, by someone able to exercise the Financials screen, since each one changes when a spinner or an error appears.
  • The four eqeqeq ones (#5 to #8) should probably never be "fixed". If they are ever cleaned up, the honest form is == null kept with an eslint-disable and a comment, not === null.

Built with VitePress