The financials rename broke lint:admin-zones โ
- Status: gate green, 2026-08-27.
npm run lint:admin-zonesexits 0, andnpm run validate -- --scope lintpasses all 16 checks. - Cause: the
apps/admin/src/admin/billing/toapps/admin/src/admin/financials/rename in6f21f04a, which this project's financials consolidation drove. - Scope:
apps/admin/src/admin/financials/andtooling/scripts/admin-zones-baseline.jsononly.
What actually broke? โ
The ratchet keys on file path, so a rename reads as twelve brand new errors. โ
tooling/scripts/lint.admin-zones.mjscompares a per-file, per-rule ERROR count againstadmin-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.jsonwas 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? โ
| # | File | Rule | Count | Verdict | Why |
|---|---|---|---|---|---|
| 1 | financials/BillingReports.jsx | react-hooks/set-state-in-effect | 1 | Remapped | Byte-identical move (zero-line diff in the rename commit), and the fix is a fetch-lifecycle restructure that changes when the spinner appears |
| 2 | financials/Financials.jsx | react-hooks/set-state-in-effect | 2 | Remapped | The 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 |
| 3 | financials/InfraCosts.jsx | react-hooks/set-state-in-effect | 1 | Fixed | Genuinely new code written today, so it never had a baseline entry to remap. See below |
| 4 | financials/components/LineItemsTable.jsx | react-hooks/set-state-in-effect | 1 | Remapped | Byte-identical move, and another session has uncommitted work in this file |
| 5 to 8 | financials/components/LineItemsTable.jsx | eqeqeq | 4 | Remapped | All 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 |
| 9 | financials/components/PeriodPicker.jsx | react-hooks/set-state-in-effect | 1 | Remapped | Byte-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 |
| 10 | financials/vendors/VendorPageShell.jsx | react-hooks/set-state-in-effect | 1 | Remapped | Byte-identical move. Same fetch-lifecycle shape as #1 |
| 11 | financials/vendors/sections/VendorLineItemsSection.jsx | react-hooks/set-state-in-effect | 1 | Remapped | Byte-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-baselinewas 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. โ
fetchCostsopened withsetHealthError(null)and latersetOtpError(null). Both moved to sit beside theirsetDatacall 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-effectflags a call out to a hook-level callback that sets state anywhere in its body. It does not trace theawaitboundary, so moving the firstsetStateafter the firstawaitleft the error exactly where it was. Verified against a throwaway probe file:useEffect(() => { cb() })fails whether or notcbis async, while anasync functiondeclared inside the effect and called there passes.fetchCostshad no caller other than that one effect, and it wasuseCallback(..., []), so its identity never changed. Deps[fetchCosts, refreshToken]reduce to[refreshToken]with no change in when the effect runs.useCallbackbecame unused and was dropped from the import; theReactimport 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.jsxrenders 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-effectfetch-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
eqeqeqones (#5 to #8) should probably never be "fixed". If they are ever cleaned up, the honest form is== nullkept with an eslint-disable and a comment, not=== null.