Refresh each watchlist independently, show its real age, and alert when it goes stale - #94
Merged
Merged
Conversation
…al age One flaky upstream used to age every list. All four sanctions feeds were required together, so a 500 from the EU webgate blocked the OFAC refresh — and OFAC is the list every visitor screens against by default. A European outage aged the data for users who never enabled EU. That happened on 2026-08-02: the EU feed 500'd for hours and the whole daily publish died. Each list is now refreshed, and therefore fails, on its own. A list whose upstream is unreachable is re-served from the bundle already published, re-verified through the full trust chain, keeping its OWN version and its OWN fetch instant, and stamped `stale` with the failure that caused it. This is NOT "feeds are optional". It is still fail-closed where it counts: a list that can be neither refreshed nor proven from the published bundle aborts the publish rather than quietly vanish from a bundle that claims it. Per-list freshness rides INSIDE the staged tree (catalog.json and each <slug>/meta.json), so it inherits the existing trust chain for free: chunk hash -> file_sha256 -> manifest hash -> signed pointer. Nothing is added to /latest, so the pointer's canonical signing preimage, its signature and its monotonic anti-rollback sequence are untouched. Already-published bundles carry no `fetchedAt`; carry-forward falls back to their `generatedAt` and refuses outright if neither can age the list — it never invents a timestamp. Contract reversed, deliberately: the test asserting that ANY unfetchable feed aborts the bundle is inverted, not deleted. Its fail-closed half is preserved as "cannot be fetched AND cannot be carried". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0186xrrT9mYfCZTVkk8HN12p
The README and DEPLOY both read as if a daily cron meant every list is under 24 hours old. It never did: upstream feeds go down, and on 2026-08-02 the EU webgate 500'd for hours. Worse, the cron failed 22 days straight in June/July on a missing signing secret and nothing said so. Replaces the cadence promise with what is actually true and now enforced: each list refreshes daily WHEN ITS SOURCE IS REACHABLE, each list shows its own last successful update time, and a separate freshness check verifies the live bundle and opens an issue when a list goes stale. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0186xrrT9mYfCZTVkk8HN12p
The client now requires every list to carry a provable age. The committed demo origin — the cold-clone artifact a fresh clone screens against with zero setup, and the v2 delta-sync e2e fixture — predated that field, so without this a fresh clone would fail to load its own bundle. Rebuilt with the real edge-proc publish path, then pruned to exactly the chunks the new manifest references. The v1/v2 dedupe property that the delta-sync test measures is preserved: 9 of 13 chunks are shared, and only OFAC's 4 changed files must be fetched on the v1 -> v2 hop. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0186xrrT9mYfCZTVkk8HN12p
…nnot state one
CLAIM TOUCHED: "you are screening against these lists" — which was silently a
claim about coverage the product could not back. One flaky upstream used to
block every list from refreshing; the publisher now refreshes each list
independently and re-serves the last good copy of any list it could not reach.
So `catalog.generatedAt` says nothing about how old any individual list is, and
until now nothing on screen did either.
Two halves, both required.
1. THE GUARDS NOW GUARD. `assertBundleListMeta` declared `generatedAt` and
`model` and never narrowed them — a guard that typed two fields it never
checked. Both narrowing functions now validate the four freshness fields as
well: `fetchedAt` must parse as a real instant, `sourceUpdatedAt` must be
null or parse, `stale` must be a boolean, `staleReason` must be null or a
string. Every field is REQUIRED. A missing `fetchedAt` is not "fetched now"
and a missing `stale` is not `false` — a bundle whose lists cannot state
their own age is rejected, because a list we cannot age is a list we cannot
honestly serve.
Proven able to fail: with `hasListFreshness` stubbed to `return true`, 19 of
the 36 tests in watchlist.bundle.test.ts + bundleSource.freshness.test.ts go
red; restored, 36 pass. Before the implementation existed, 23 went red.
2. THE METADATA REACHES THE SCREEN. `CatalogListInfo` was `{id, title}`, and
`EngineRuntime#catalogLists` projected the signed catalog entry down to those
two fields — which is precisely why no UI could show an age. It now carries
version, entity count and the four freshness fields, and Settings renders
them beside each toggle: "updated 4 hours ago" when fresh, and
"Not updated for 3 days — the EU feed was unreachable" when not.
The warning is text inside the checkbox's own label, so it is part of the
control's accessible name; the colour and the warning glyph only reinforce
it. An unparseable `fetchedAt` renders "Age unknown", never "fresh" — the
same fail-closed rule as the engine guard, restated at the render boundary.
Relative ages go through `Intl.RelativeTimeFormat`/`Intl.NumberFormat` in a pure
helper (`listAge`), so the wording follows the viewer's locale without a key per
unit; the surrounding sentences live in the i18n catalogue.
Tests: +52 (browser 460 -> 489, app 403 -> 426). Coverage on the changed files:
bundleSource.ts 98.4% stmts / 96.9% branch, SettingsPage.tsx 98.6% / 92.6%,
watchlist.ts 93.8% / 90.5%.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186xrrT9mYfCZTVkk8HN12p
`edgeproc publish` writes three things alongside the served contract: a producer-side CAS mirror (chunks/, manifests/) and a .mutation.lock. The served contract is only latest + manifest/<hash> + chunk/<hash>. Two of the three were already deleted by the builders; .mutation.lock was deleted by nobody. It would have been committed with the demo bundle and published to the CDN at /bundle/origin/.mutation.lock. Fixes it at the source — one shared removeProducerResidue() used by the real and both demo builders — with .gitignore as the backstop for an interrupted or hand-run publish. Build output belongs in .gitignore, never in a commit. Verified: both demo bundles rebuild byte-identically (git reports no diff after a full regen) and leave no residue behind. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0186xrrT9mYfCZTVkk8HN12p
…e new format to prove itself Two fixes to the staleness guard, both the same class of bug. 1. NO FALSE ALARM ON DAY ONE. The guard failed any list without a per-list `fetchedAt` — which is every list in the bundle live right now, because that field only starts appearing after the first publish on this branch. It would have opened an issue against a bundle that is actually fine, and a guard that cries wolf immediately gets muted. An ABSENT `fetchedAt` now falls back to the catalog's `generatedAt`, mirroring carryForwardList.publishedFetchedAt(). That is truthful: in the pre-freshness model all four lists were refreshed together in one run. The output names the anchor it used (`source` column + a footnote), so a reader is never misled about where the number came from. The fallback is deliberately narrow. A `fetchedAt` that is PRESENT but empty, unparseable, a number or null gets no fallback — that is corruption, not migration, and "cannot tell" must keep meaning REJECT. No anchor at all still fails. Contract reversed, deliberately: the test asserting that an absent `fetchedAt` ALWAYS fails is inverted, not deleted. Its fail-closed half is preserved and split — absent WITH no generatedAt still fails, and present-but-malformed still fails. 2. THE NEW FORMAT MUST CARRY ITS PROOF. `stale: wire.stale === true` read a MISSING `stale` as "not stale": drop or corrupt one field and the list reported itself healthy — shape checked, property not. Presence of `fetchedAt` now decides which fields are mandatory. A new-format entry must carry `stale` as a real boolean, `staleReason` as string|null, and `entitiesCount` as a number; anything else is a malformed catalog and fails without coercion. A legacy entry is held to none of it. `entitiesCount` no longer falls back to a -1 sentinel that then gets printed in the table. It is null, rendered "unknown". Proof the guards can fail — five mutations, each red, restored green: laundering a malformed fetchedAt through the fallback (7 red), not judging a generatedAt-aged list by age (1 red), treating "no anchor" as fine (2 red), dropping the new-format field checks (10 red), imposing them on legacy entries (3 red). LIVE PRODUCTION, aml-filter.com/bundle/origin, version 2026-08-01 sequence 50: PASSES at the default 26h ceiling reporting 19.0h from generatedAt across all four lists, and goes RED at --max-age-hours 1 naming every list with its real 19.0h age. The guard bites on real bytes, not only on fixtures. 50 tests in this suite, 317 in the package, 100% line / 92.4% branch on the module. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0186xrrT9mYfCZTVkk8HN12p
…emits The freshness fixtures used `UK_HMT`. Nothing emits that id — the publisher declares `UK_LIST_ID = "UK_OFSI"`, the live bundle publishes `UK_OFSI`, and every client-side test expects `UK_OFSI`. The guard iterates the catalog rather than matching ids, so it could still fire. But a fixture keyed to an id production never emits is the shape of the defect this workstream exists to remove: test data that cannot disagree with reality because it never touches it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0186xrrT9mYfCZTVkk8HN12p
CLAIM TOUCHED: "It is matched against the public OFAC sanctions list right here in your browser", plus the boot banner's "List verified." "List verified." is a statement about the SIGNATURE. Readers take it as a statement about the DATA. A perfectly-signed three-day-old OFAC copy is still three days old, and this route pins one list with no selector to hang a badge off, so a visitor had no way to find that out. The highest-traffic surface in the product was the one claiming coverage with no age at all. The age now sits directly under the boot banner, next to the search box: "OFAC SDN · updated 6 hours ago" when fresh — quiet, one line. When the publisher could not refresh the list it becomes an unmissable panel carrying the REAL age and the reason, with role="alert" (it appears after load and changes what the result on screen means) and the warning in the words, not the colour. An age that cannot be read says "Age unknown". It never falls back to silence, because silence next to "List verified." reads as "current" — a failed catalog read and an id the catalog does not carry both resolve to the same honest answer. Reading the age never blocks screening: `readScreenedList` cannot throw. The boot scope and the age line now read one constant (`SCREENED_LIST`), so the page cannot describe a list it did not load. `enabledLists` and the streaming residency are unchanged — that scoping is a deliberate iOS memory decision. Six ScreenPage test mocks gained `catalogLists()`. They had drifted from the real runtime, and the page correctly rendered "age unknown" against them; a mock that lags the contract stops testing it. Proven able to fail: force the fresh branch (`if (true)`) so a stale list renders as freshly updated, and 3 of the 7 new tests go red — the reason text, the announced alert, and the "does not present a stale list as fresh" case. Restored, 7 pass. Before the implementation existed all 7 were red. Tests: +7 (app 426 -> 433). i18n parity + live-drive green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0186xrrT9mYfCZTVkk8HN12p
The catalog read is awaited AFTER the page is already interactive, so the component can unmount during it. Guard the state write the same way the boot path already guards its own. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0186xrrT9mYfCZTVkk8HN12p
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
The new removeProducerResidue import was inserted in the wrong alphabetical slot, which biome's organize-imports rule rejects. CI caught it; my local lint pass did not, because I piped biome through `tail -1` and read the last line instead of the exit code — a check that could not fail. Re-verified the way CI does it: `pnpm -r run lint`, `typecheck`, `test:coverage` and `build` all exit 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0186xrrT9mYfCZTVkk8HN12p
…jecting the live bundle
THE HAZARD. `isBundleCatalogList` required a per-list `fetchedAt` on every
catalog entry, and `assertBundleCatalog` runs on the bundle-open path. No bundle
published before this branch carries one — the LIVE aml-filter.com catalog
(version 2026-08-01, sequence 50) has `fetchedAt: undefined` on all four lists.
Shipping that client while the origin serves a pre-freshness bundle stops the
deployed app loading its watchlist at all, and `deploy.yml`'s
`mirror-published-origin` fallback re-serves exactly that old signed bundle
whenever a fresh build cannot be produced. Never ship enforcement ahead of its
migration.
ONE RULE, THREE CONSUMERS. The same rule existed three times — the browser's
narrow, `publishedFetchedAt`, and the staleness gate's age anchor — and two of
them already disagreed on the live bytes. `resolveListAge` / `resolveListFreshness`
in amlfilter-browser/src/engine/watchlist.ts is now the only copy, exported at
`@amlfilter/browser/watchlist` and read by all three:
fetchedAt present + parseable -> aged from fetchedAt
fetchedAt ABSENT -> legacy bundle: aged from catalog generatedAt
fetchedAt present + malformed -> REJECT, never laundered into the fallback
`CatalogListInfo` now carries `agedFrom`, and Settings + /screen word a legacy
list's age as the bundle's build time ("in a bundle built 19 hours ago") instead
of "Age unknown" or a per-list refresh time we cannot prove. Signature,
content-address and anti-rollback verification are untouched.
TWO CONTRACTS REVERSED, deliberately: the tests asserting that a missing
`fetchedAt` must be REJECTED asserted the defect. They are inverted, not
deleted, and say so in place.
EVIDENCE
- Live origin, both paths, same bytes: browser `openBundleSource` and the
publisher now both ACCEPT all four lists, aged from generatedAt
@ 2026-08-01T18:23:54.891Z. Before: browser REJECT / publisher ACCEPT.
- Cross-implementation parity suite (freshnessParity.test.ts, 8 tests) feeds one
entry through openBundleSource, publishedFetchedAt and checkPublishedFreshness
and asserts identical verdicts across legacy, new-format, "", "whenever",
12345, and a missing catalog generatedAt.
- Mutation proof: making a malformed fetchedAt fall through to generatedAt turns
23 tests red across both packages (13 browser, 10 publisher, incl. 3 parity
cases). Restored.
- Driven live in Chrome: the app built against a `mirror-published-origin` copy
of the LIVE legacy bundle loads 19,181 OFAC entities, screens in 515 ms, shows
all four lists' ages on Settings, clean console.
- Gate: lint=0 typecheck=0 test:coverage=0 build=0 (1,461 tests).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186xrrT9mYfCZTVkk8HN12p
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The incident this fixes
The daily
publish-watchlistcron has been failing onEU_CONSOLIDATED: required feed fetch failed: EU request failed: 500 Internal Server Error. The EU webgate is still500ing.
The publisher was right to refuse. It failed closed rather than sign a bundle missing
a required list, and that behaviour is preserved. The bug is the coupling: all four
feeds were required together, so a European outage blocked the OFAC refresh — and OFAC
is the list every visitor screens against by default. Users who never enabled EU were
getting stale data because of a European outage.
Claim touched
Both were claims about coverage and freshness that nothing enforced.
1. Per-list independence, fail-closed preserved per list
Each list is now refreshed — and therefore fails — on its own. A list whose upstream is
unreachable is re-served from the bundle already published: fetched back through the
full trust chain (signed pointer against the pinned key → manifest content-address →
every chunk through the client's zstd-decompress → sha256 → each file against its
file_sha256), keeping its own olderversionand its ownfetchedAt, stampedstalewith the upstream failure verbatim.This is not "feeds are optional". A list that can be neither refreshed nor proven
from the published bundle still aborts the publish. Nothing is dropped from the catalog,
so a bundle can never assert coverage it does not have.
Contract reversed, deliberately. The test asserting that any unfetchable feed aborts
the bundle was inverted, not deleted — its fail-closed half survives as "cannot be
fetched AND cannot be carried".
No format change to the signed pointer
Per-list freshness rides inside the staged tree (
catalog.jsonand each<slug>/meta.json), so it inherits the existing trust chain for free:chunk hash →
file_sha256→ manifest hash → signed pointer. Nothing is added to/latest, so its canonical signing preimage, its signature, and its monotonicanti-rollback
sequenceare untouched.Already-published bundles. Every bundle published before this carries no
fetchedAt.Carry-forward and the freshness guard both fall back to that bundle's
generatedAt—truthful, because in the old model all four lists refreshed in one run — and refuse
outright if neither can age the list. A present-but-malformed
fetchedAtis corruption,not migration, and still fails. No timestamp is ever invented.
2. The age is on screen
CatalogListInfowas{id, title}, which is precisely why no UI could show an age. Itnow carries version, entity count and the four freshness fields. Settings renders
"updated 4 hours ago" when fresh and "Not updated for 3 days — the EU feed was
unreachable" when not, as text inside the checkbox's own label (part of its accessible
name, not colour alone). An unparseable
fetchedAtrenders "Age unknown", never "fresh".The client guards now actually guard:
assertBundleListMetadeclaredgeneratedAtandmodeland never narrowed them. All four freshness fields are now required and validated.3. The freshness guard
watchlist-freshness.ymlruns 4×/day, verifies the live pointer's signature, readscatalog.jsonback through the client's decode path, grades bundle-level and per-listages against a 26h ceiling, and opens/updates a single GitHub issue on breach.
It deliberately does not rely on being noticed — a red scheduled run is exactly the signal
that failed here. The alert step runs first; the failing step runs last.
Evidence
version 2026-08-01,sequence 50,generatedAt 2026-08-01T18:23:54.891Z→ 19.0h at check time. Pointer servedapplication/json, signature-verified.carryForwardListagainstaml-filter.com: 6,239 EU entities, vectors 2,395,776 = 6239×384, version kept at2026-08-01, aged 18.8h via thegeneratedAtmigration path,stale: true.--max-age-hours 1→ exit 1, naming all four lists at their real 19.0h age.delta-sync.spec.ts+scoped-sync-topup.spec.ts→ 2 passed in real headless Chromium. Static: cold scoped sync 4/13 chunks ((root)+ofaconly), EU top-up 3 chunks (euonly), re-fetched already-local chunks =[].git statusempty) and leaves no.mutation.lock/chunks//manifests/.The live red run
Mutation proof — the decoupling tests can fail
fetchedAt: now(the "looks fresh" lie)stale: falseversionThe first row is the important one: a per-list staleness test that passed because the list
was absent would prove nothing. Dropping the list turns it red.
Client side: stubbing
hasListFreshnesstoreturn trueturns 19 of 36 browser testsred.
Cron failure history — worse than it looked
46 scheduled runs since 2026-06-21, 25 failures:
WATCHLIST_SIGNING_KEY secret is not setFeed flakiness is 2 in 46. The headline is the other number: the publish job was
dead for 22 straight days and nobody noticed, because a failed scheduled run notifies no
one. That is the case for the guard, independent of the decoupling.
EU feed: transient outage, not a moved endpoint
Every FSD format 500s (
xmlFullSanctionsList_1_1,xmlFullSanctionsList, both CSVvariants,
xmlActiveSanctionsList_1_1); the path is unchanged; the static token is stillaccepted (no 401/403 on file paths); the body is the webgate's own JSON error; and
08-01 succeeded through this exact URL.
fetchWithTimeoutalready retries 3× withbackoff and treats 500 as retryable — those retries were exhausted in ~47s. Retrying
harder is not the fix; decoupling is.
Docs
The README/DEPLOY "runs daily" wording implied every list is under 24h old. Replaced with
what is true and now enforced: each list is refreshed daily when its source is reachable,
and every list shows its own last successful update time.
Deliberately not done
Per-list staleness has no hard ceiling at which coverage is withdrawn. Past some age a
carried list is arguably worse than an absent one — but refusing to publish would re-couple
the lists and age OFAC again, which is the defect being fixed. Left to the guard (red at
26h) on purpose; flagging it as the one policy call worth a second opinion.
🤖 Generated with Claude Code
https://claude.ai/code/session_0186xrrT9mYfCZTVkk8HN12p
Deploy hazard found and fixed mid-review
Making the client require per-list
fetchedAtwould have rejected the bundle productionis serving right now. Verified by running the live bytes through the branch's own
predicate, not by reading source:
isBundleCatalogListgates onhasListFreshness, andassertBundleCatalogis on thebundle-open path — so a strict client paired with a pre-freshness bundle stops loading the
watchlist. That pairing is reachable:
deploy.yml'smirror-published-originfallbackre-serves the OLD signed bundle whenever a fresh one cannot be built.
Fixed as a migration, not a flip — enforcement must never ship ahead of the data it
enforces. A legacy entry with no
fetchedAtis aged from the catalog'sgeneratedAt; afetchedAtthat is present but malformed is still rejected, so the fallback cannot laundercorruption. The removal condition is recorded in the code: once every published bundle
carries per-list
fetchedAt, the fallback comes out of all call sites together.The same rule now lives in one shared predicate consumed by both the client and the
publisher, with a cross-implementation test running identical inputs through both and
asserting identical verdicts. Two copies of one rule diverge — that has already produced a
real defect in this portfolio.
The stale panel, rendered in a real browser
Not inferred from shared logic. A bundle variant with OFAC stale was published, served
under the production CSP, and
/screenwas driven in headless Chromium:The
elementFromPointcheck is deliberate. Arole="alert"panel can be perfectlycorrect in logic and still be painted over by a
stickynav that comes later in the DOM —that exact bug survived months in a sibling repo while every unit test passed. This one
owns its own centre point.
Screenshots:
screen-STALE-panel.png(stale/screen),settings-freshness.png(per-list ages in Settings, EU flagged red),
screen-list-age.png(fresh/screen).Chunk-dedupe blocker — closed, with before/after
The 46.6 → 28.2 MB cold-download cut rests on one property: enabling a list later
re-fetches nothing already local, asserted as an empty set intersection. The bundle
regeneration could have moved chunk boundaries. It did not:
main)(root)+ofaceu(root)+ofaceuOnly the cold plaintext size moved, 6,043 → 6,642 bytes (+599) — the freshness fields
themselves. Chunk topology is identical because the tree is still one chunk per file.
Confirmed at runtime, not just statically:
delta-sync.spec.ts+scoped-sync-topup.spec.ts→ 2 passed in real headless Chromium, on a pinned portwith
reuseExistingServerdisabled so no other agent's server could be measured bymistake.
What was NOT verified — stated plainly
fetchedAt, so that branch is covered by unit tests only.viewport. Four live-driven cases; do not read them as six.
/screenstale flow, and the listener was proven live by making the page emit adeliberate
console.errorand asserting it was caught. A zero from an unprovenlistener is worth nothing.
Follow-up with a dated trigger, not a TODO
The shared freshness predicate is deliberately permissive right now: an absent
per-list
fetchedAtfalls back to the catalog'sgeneratedAt. That is required whileproduction still serves a pre-freshness bundle.
Flip it to strict when the live origin carries per-list
fetchedAt— check with:Because the rule lives in one shared predicate with two callers, that flip is one change
in one file and both sides move together. There is never a moment where the CI gate
accepts what the app rejects.
Hazard: before / after, same live bytes
https://aml-filter.com/bundle/origin,version=2026-08-01 sequence=50,catalog.generatedAt=2026-08-01T18:23:54.891Z, no per-listfetchedAton any entry.generatedAt)One predicate, two callers, verified against production bytes rather than fixtures.
freshnessParity.test.tsruns identical inputs through both and asserts equal verdicts, sothey cannot drift apart again. Mutating the shared rule to let a malformed
fetchedAtfallthrough to
generatedAtturns 23 tests red across both packages.CI on the final commit: frontend pass (4m35s), gitleaks pass, GitGuardian pass.