Skip to content

Agent-first repo: strict TypeScript, god-file splits, 10 test layers, CI pipeline#127

Closed
passandscore wants to merge 55 commits into
mainfrom
agentic-repo-design
Closed

Agent-first repo: strict TypeScript, god-file splits, 10 test layers, CI pipeline#127
passandscore wants to merge 55 commits into
mainfrom
agentic-repo-design

Conversation

@passandscore

@passandscore passandscore commented Apr 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Turns the Fast Protocol App into a production agent-first codebase. Over 47 commits, the repo now has:

  • Three-tier agent documentation (.claude/skills, agent_docs/, user-facing docs/).
  • Full strict-mode TypeScript (strictNullChecks, noImplicitAny, noUnusedLocals, noUnusedParameters).
  • Two god-file splits (LeaderboardTable 2711→447 LoC, SwapConfirmationModal 1158→623 LoC) into 18 leaf components + 3 hooks.
  • All 52 input-taking API routes validated through Zod + a discriminated-union @/lib/api/parse helper.
  • Testing across 10 layers (unit, property, integration, cross-module invariants, ABI drift, EIP-712, hook tests, a11y, fork, mutation).
  • CI workflows for format, verify, build (path-gated), fork (nightly), externals (weekly), mutation (weekly).
  • External-workspace pattern (.external/mev-commit vendored read-only), Dependabot, Stryker widened to four pure modules.
  • One security bug fix caught by in-branch review (fastswap/route.ts was spreading body instead of body.data post-parse-rewrite).

Diff: 326 files, +18,276 / −5,150. Tests: 342 passing across 28 files (was 150ish on main).

Why this PR is safe to land despite its size

  1. No behavior changes in the refactors. The two god-file splits (commits 31f407e, 242ae7d, 594c823) and the parse.ts rewrite (6a83b73) are pure code-move + type-tightening. Every extracted file preserves className, tooltip text, and conditional logic verbatim. Commit messages call this out specifically.
  2. TypeScript caught the hard cases. The strictNullChecks flip surfaced 40+ null/undefined mismatches (6a83b73), every one traced to a real call-site bug hidden by loose types. Zero new errors on the subsequent strict: true flip (4ef4f8a).
  3. Every route's Zod migration is symmetric. The ESLint rule at eslint.config.js:88-112 forbids imperative validation (request.json(), nextUrl.searchParams) on route files — zero warnings across all 52 routes.
  4. Security review was run on this diff. See "Verification evidence" below; the only HIGH finding was fixed in f1bcfce.
  5. UI smoke passed on /, /dashboard, /leaderboard, /claim — all HTTP 200 with correct content markers.

Major changes by area

Agent infrastructure (new)

  • .claude/skills/ — per-domain how-to (next-app-router, defi-swap, web3-wallet, dashboard-data, leaderboard-miles, contract-abis, ui-shadcn, testing-vitest, skill-creator, external-mev-commit).
  • .claude/agents/ — subagents (explore-web3, security-reviewer, ui-verifier, abi-tracer).
  • .claude/commands/ — slash commands (/prime, /verify, /verify-ui, /typecheck, /lint, /test, /review-diff, /sync-externals, /new-skill).
  • .claude/hooks/ — PostToolUse typecheck + mirror-test + conditional build; Stop format:check.
  • agent_docs/ — stack, architecture, verification, glossary, db-schema, route indexes, audit-followup, external-mev-commit.
  • AGENTS.md — portable agent spec (CLAUDE.md delegates to it).
  • .external/mev-commit/ — upstream source-of-truth vendored read-only via .claude/externals.yml.

TypeScript strictness

  • noImplicitAny: true (30edd51)
  • noUnusedLocals: true, noUnusedParameters: true + purged 77 dead declarations (7c96d1c)
  • strictNullChecks: true + fixed 40+ sites across hooks, components, routes (6a83b73)
  • strict: true — zero new errors (4ef4f8a)

API validation

  • src/lib/api/parse.tsparseJson / parseSearchParams / parseParams returning { ok: true; data } | { ok: false; response }.
  • src/lib/api/schemas.ts — shared Zod primitives (walletAddressSchema, txHashSchema, tokenSymbolSchema, paginationSchema).
  • 52 routes migrated (several commits). ESLint rule flags regressions.
  • 32 routes migrated from instanceof NextResponse to the discriminated union; all callers updated.

God-file splits

  • LeaderboardTable 2711 → 447 LoC (−84%). 11 leaves under src/components/dashboard/leaderboard/: LeaderboardRow, PaginatedLeaderboardModal, Volume/Efficiency/Referral/RisingStars cards (phase 1), LeaderboardHeader, Volume/MilesProgressAnalysis, Volume/MilesModeTable (phase 2), plus types.ts.
  • SwapConfirmationModal 1158 → 623 LoC (−46%). 7 leaves under src/components/modals/swap-confirmation/: InfoRow, BuyReceiveValue, TransactionSummary, SwapDetailsCollapse, ErrorView, ErrorDetailModal, ConfirmCtaButton, plus useSnapshotOnOpen hook + shared style.

Test coverage

  • Unit / example tests for every pure module (src/lib/swap/**, src/lib/tokens/**, src/lib/settlement/**, src/lib/api/**).
  • Property tests (fast-check) for slippage math, min-amount-out picker, Zod schemas.
  • Integration tests via pg-mem for one route (window-function limitation noted in audit-followup).
  • ABI drift test comparing local ABIs against vendored upstream.
  • EIP-712 / Permit2 DOMAIN_SEPARATOR encoding test.
  • Hook tests (happy-dom + renderHook) for use-swap-slippage, use-quote-guard-config, use-balance-flash, use-page-active, use-debounced-validating, use-swap-form.
  • Component tests for SwapToast, SwapConfirmationModal, LeaderboardHeader, ErrorView, useSnapshotOnOpen.
  • A11y tier (tests/a11y/, axe-core via vitest-axe) seeded on SwapToast.
  • Fork tests (tests/fork/permit2.fork.test.ts) — skipped unless FORK_RPC_URL is set; nightly CI runs with ethereum-rpc.publicnode.com.
  • Mutation tests (Stryker) over slippage.ts, min-amount-out.ts, api/schemas.ts, tokens/token-resolver.ts. Score: 83.5% overall.

Verification evidence

Run locally (all green before this PR was opened):

npm run typecheck       # tsc --noEmit
npm run lint            # next lint
npm run test:run        # 342 passing / 3 skipped
npm run format:check

Or: /verify (slash command runs the first four).

Security review (on this diff)

/review-diffsecurity-reviewer subagent scanned the full main..HEAD diff. Findings:

  • HIGH (fixed in f1bcfce): src/app/api/fastswap/route.ts:41 was spreading the ParseResult wrapper (body) into the upstream request instead of the parsed data (body.data). Caught post-strictNullChecks rewrite. No prod impact today (route is unused) but would have broken on re-enable.
  • MEDIUM: useSnapshotOnOpen shallow spread could leak tokenIn/tokenOut mutations. Low likelihood in this codebase (tokens are replaced by reference, not mutated), documented in the hook comment.
  • LOW / nit: SwapToast has four render-body console.logs that would double-fire under React strict mode; fastswap nonce schema accepts 0 (imprecise but not exploitable); fuul/leaderboard stale-cache path re-reads searchParams intentionally.
  • Cleared: parse.ts discriminated-union narrowing, pre-validation input leaks, permit2 safety, slippage bounds, eth-path throw contract, WETH calldata, env/secrets, SQL injection, no new runtime deps outside tooling.

UI smoke (ui-verifier subagent)

/             HTTP 200  content: matched ("Swap" + animated background)
/dashboard    HTTP 200  content: matched (personal miles page)
/leaderboard  HTTP 200  content: matched ("LEADERBOARD", Gold/Silver/Bronze, Volume/Stats)
/claim        HTTP 200  content: matched ("Genesis", "Claim")

No console errors in SSR output on any route. Analytics API 401s without crashing (graceful skeleton / empty states). No real Application error markers anywhere.

Mutation testing (Stryker)

All files           83.50%   167 killed   26 survived
  schemas.ts        100.00%   25 killed    0 survived
  slippage.ts        96.30%   26 killed    1 survived (equivalent)
  min-amount-out.ts  80.95%   17 killed    4 survived (equivalent)
  token-resolver.ts  77.95%   99 killed   21 survived

Survivors in slippage.ts and min-amount-out.ts are equivalent mutants (both branches produce the same output at boundary values — unkillable without rewriting the source). token-resolver.ts has genuine test gaps noted in agent_docs/audit-followup.md; below the 80% bar but above the 70% low threshold.

Known caveats (not blockers, but worth knowing)

  • Visual parity not independently verified. All tests + strictNullChecks + security review cannot catch pixel-level drift. Recommend a human click-through of /leaderboard (all three mode tabs + "All Leaders" modal) and the swap confirmation modal (connect wallet → swap → open modal) before merging.
  • Component tests still thin on the extracted leaves. 5 of the 18 leaves have direct tests; the rest are indirectly exercised via their parents and the UI smoke. Natural adds as they're touched next.
  • Hook tests for use-swap-form pin state contracts and the wallet-disconnect reset, but derived values (activeQuote, computedMinAmountOut) require deeper mocks and live in follow-up.
  • Fork tests skip locally without FORK_RPC_URL; the nightly CI workflow runs them against ethereum-rpc.publicnode.com.

Follow-ups (tracked in agent_docs/audit-followup.md)

  • Component tests for VolumeModeTable, MilesModeTable, VolumeProgressAnalysis.
  • Widen mutation coverage (token-resolver from 78 → 90+ by killing non-equivalent survivors).
  • Bundle-size budget on PRs (policy decision).
  • Accessibility sweep beyond SwapToast.
  • Error taxonomy document (needs ownership).

Test plan

  • npm run typecheck passes.
  • npm run lint passes.
  • npm run test:run passes (342 / 3 skipped).
  • npm run format:check passes.
  • Human click-through on /leaderboard across Miles / Volume / Stats tabs (+ tier filter on Volume, "All Leaders" modal).
  • Human click-through on swap flow: connect wallet, build a quote, open the confirmation modal, verify numbers match the toast post-submit.
  • Optional: FORK_RPC_URL=https://ethereum-rpc.publicnode.com npm run test:fork green.

Project uses npm — CI (.github/workflows/format.yml) runs npm ci and
package-lock.json is authoritative. The bun.lockb was a redundant second
lockfile; tsconfig.tsbuildinfo is a local TS incremental build cache that
shouldn't be committed. Added both patterns to .gitignore so they stay out.
Layer retrieval and attention-budget optimizations on top of existing code
— nothing under src/, contracts/, or contracts-abi/ changes. Distilled from
Anthropic's context-engineering + Agent Skills guidance and HumanLayer's
CLAUDE.md / harness-engineering posts.

Root orientation (progressive disclosure entry points):
- AGENTS.md: portable spec (Codex/Cursor/Amp/Claude)
- CLAUDE.md: Claude-specific additions, imports AGENTS.md via @
- README.md: link to the agentic surface

Reference layer (agent_docs/, open on demand):
- stack, architecture, web3-integration, contracts-and-abis,
  env-vars, testing, verification, glossary

.claude/ infrastructure:
- skills/: 9 domain skills (skill-creator, next-app-router, defi-swap,
  web3-wallet, dashboard-data, leaderboard-miles, contract-abis, ui-shadcn,
  testing-vitest) — each with SKILL.md + reference files, no inlined code
- agents/: 4 subagents as context firewalls (explore-web3,
  security-reviewer, ui-verifier, abi-tracer)
- commands/: /prime, /verify, /typecheck, /lint, /test, /new-skill,
  /review-diff
- hooks/: PostToolUse typecheck + Stop format-check, silent on success
- settings.json: permission allow/deny lists + hook wiring

Verification loop:
- package.json: add `typecheck` script (tsc --noEmit) so /verify has the
  full typecheck + lint + test:run stack
.superset/config.json is local worktree setup/teardown scaffold (the dir
holds Claude Code's per-repo worktree config). Not shared config; untrack
and ignore.
…PI layer

Executes the agent-efficiency audit punch list:

- Delete 570 LoC of unreferenced `fast-settlement-*.ts` and the 77-line dead
  `fetchQuote` in `use-swap-quote.ts`.
- Folderize `src/lib/` into `tokens/`, `swap/`, `settlement/`, `config/`;
  flatten the one-file `swap-logic/` folder. Rewrite ~130 `@/lib/*` imports.
- Replace the half-barrel `src/hooks/index.ts` (9 of 52 hooks) with a full
  `export *` barrel. Deduplicate the `UserOnboardingData` type collision.
- Introduce `@/lib/api/{parse,schemas}` Zod helpers. Migrate 24 of 53 API
  routes to use them; structured 400 responses with `issues[]`.
- Move tests out of `src/**/__tests__/` into a clean top-level `tests/`
  mirroring `src/`. Add 50 new tests (50 → 100) covering Zod primitives,
  parse helpers, token-resolver, weth-utils, stablecoins, and pagination.
  Update `vitest.config.ts` accordingly.
- Add two PostToolUse hooks: `post-edit-test.sh` (runs the mirror test for
  the edited file) and `post-edit-build.sh` (runs `next build` when the
  edit touches API routes, middleware, env, or server actions).
- Codify the three-tier doc convention in `.claude/README.md`: skills own
  how-to, `agent_docs/` owns the map, `docs/` owns human deep-dives
  (banner-gated). Update every stale `src/lib/*` path in skills and
  agent_docs to the new folderized layout.
- Comment hygiene on the hottest files: explain WHY behind Brave/Rabby
  detection ordering in `wallet-provider.ts`, ref-plumbing in
  `use-swap-quote.ts`, and the new api parse helper shape.
- Document outstanding work in `agent_docs/audit-followup.md`.

Verification: typecheck clean; 100 tests green; `npm run format:check` clean.
Expands the testing strategy beyond unit tests so the suite catches
classes of bugs the old layer never could.

- Install fast-check (property testing) and pg-mem (in-memory Postgres).
- Extract pure slippage math into `src/lib/swap/slippage.ts`
  (validateSlippage, slippageBpsFromPercent, computeSlippageLimit) so it
  can be property-tested without React.
- `tests/utils/arbitraries.ts`: shared fast-check generators for the
  repo's real domain (validWalletAddress, validTxHash, validTokenSymbol,
  bigUint128, slippageBps).
- `tests/utils/pg-mem.ts`: real Postgres factory with schema helpers for
  user_onboarding and user_activity. Replaces mocked pool.query so SQL
  typos, param-order bugs, and ON CONFLICT semantics surface at test time.
- Property tests for every Zod primitive (idempotence, bounds, totality,
  rejection) and every pure detector (wrap/unwrap mutex, stablecoin
  case-insensitivity, token-resolver ETH→WETH rewrite, pagination
  navigation contracts).
- Slippage property tests lock the contract-side invariants that protect
  against on-chain user loss: exactIn limit ≤ amountOut, exactOut
  limit ≥ amountIn, monotonicity, scale-invariance, non-negativity.
- `tests/api/user-onboarding.integration.test.ts`: 7 tests against real
  SQL covering GET/POST/PUT/upsert/mixed-case-normalization.
- `tests/contracts-abi/abi-drift.test.ts`: validates contracts-abi/abi/*
  JSON files and cross-checks WETH/ERC20 const ABIs against canonical
  signatures.
- `tests/invariants/cross-module.test.ts`: 11 cross-module properties
  including wallet schema ↔ viem.isAddress coherence, parse-helper 400
  shape, ETH/WETH resolution contract, and pagination navigation rules.

Verification: 185 tests across 13 files (up from 100 / 9). Full suite
runs in ~650ms. Typecheck + format clean.
Two new chain- and integration-adjacent layers:

- `tests/lib/swap/permit2-utils.test.ts` — 15 tests that lock the
  EIP-712 signing surface. Structural stability of the three type
  blocks (PermitWitnessTransferFrom, TokenPermissions, Intent),
  golden-hash snapshot for a fixed (domain, types, message) via
  viem's `hashTypedData`, byte-exact snapshot of the witness type
  string's keccak256, and property-based injectivity on user, nonce,
  and deadline. A regression here is a silent on-chain revert on
  every swap — these tests catch it at `npm test`.
- `src/lib/api/upstream.ts` — Zod schemas for Fuul leaderboard,
  Fuul payouts totals, and Barter route responses. These make it
  feasible for the routes to validate upstream data before touching
  it, and give us a typed contract instead of `any`-shaped blobs.
- `tests/fixtures/upstream/` — stored representative JSON bodies
  for each upstream endpoint (realistic, legacy-field, and minimal
  variants). Updating a fixture is how we ratify an upstream shape
  change; commit review makes the drift visible.
- `tests/lib/api/upstream.test.ts` — 9 tests that feed each fixture
  through the Zod schema and assert shape assumptions. Catches
  breaking upstream changes in CI before they produce blank UIs
  in production.

Verification: 200 → 209 tests across 15 files. Typecheck + format clean.
Closes the loop on the upstream contract tests landed in the last commit.
The routes now consume `@/lib/api/upstream` schemas at runtime, not just
in tests, so Fuul/Barter shape drift produces a structured 502 instead of
propagating undefined into the UI.

- `src/app/api/fuul/leaderboard/route.ts`: `fetchFuulPage` safeParses each
  page against `fuulLeaderboardResponseSchema`. A parse failure is treated
  exactly like a network failure — returns null so the caller serves the
  stale cache instead of ingesting corrupt entries.
- `src/app/api/fuul/payouts/route.ts`: collapses the imperative fallback
  chain (`total_points ?? total_payouts ?? total ?? points`) into a
  single schema parse + coalesce. Zod's coerce handles the historical
  string-vs-number inconsistency.
- `src/app/api/barter/route/route.ts`: the outputWithGasAmount /
  gasEstimation pair is now validated by `barterRouteResponseSchema`;
  missing required fields produce 502 instead of the old imperative
  `null`-check that returned 500.

No test changes (existing fixtures already cover these shapes via the
`upstream.test.ts` suite added in the prior commit).
Adds a domain-only EIP-712 snapshot alongside the existing full-hash
GOLDEN. The domain separator is what the on-chain Permit2 contract at
0x000000000022D473030F116dDEE9F6B43aC78BA3 returns from
`DOMAIN_SEPARATOR()`; locking it isolates "wrong domain" drift (chainId,
verifyingContract, or name typo) from the larger GOLDEN that mixes
domain and message.

- Inline snapshot for the mainnet domain separator:
  0x866a5aba21966af95d6c7ab78eb2b2fc913915c28be3b9aa07cc04ff903e3f28
- Sanity checks that swapping chainId or verifyingContract produces a
  different hash, so the test can't pass vacuously.
- Documents the pg-mem limitation (no `ROW_NUMBER() OVER(...)`) in
  agent_docs/audit-followup.md. The user-community-activity routes
  therefore can't get in-process integration coverage until we move
  to testcontainers + a real Postgres.

Verification: 209 → 212 tests across 15 files. Typecheck + format clean.
Adds the three remaining gold-standard testing layers I'd left for
later. Each is opt-in through a dedicated npm script so the default
`test:run` stays fast and network-independent.

- **Fork tests** (`test:fork`) — `tests/fork/permit2.fork.test.ts`
  spawns anvil forking from `$FORK_RPC_URL`, calls the canonical
  Permit2 contract's `DOMAIN_SEPARATOR()` view, and asserts it equals
  the inline snapshot pinned in `tests/lib/swap/permit2-utils.test.ts`.
  A second test reconstructs the domain separator from the EIP-712
  primitives (typehash, nameHash, chainId, verifyingContract) via
  viem's `encodeAbiParameters` and asserts byte-identity with both
  the on-chain value and the snapshot. Gated on `FORK_RPC_URL` so
  default runs skip — known-good endpoint is publicnode.

- **Hook tests** (default `test:run`) — installs
  `@testing-library/react` + `happy-dom` and seeds 17 tests for
  `use-swap-slippage` using `renderHook` + `act`. Per-file
  `@vitest-environment happy-dom` keeps the rest of the suite
  node-native. Covers clamp math, trailing-dot typing UX, localStorage
  round-trip, and the 5/1440-minute deadline guards.

- **Mutation testing** (`test:mutation`) — configures Stryker 9 with
  the vitest runner, scoped to `src/lib/swap/slippage.ts` where
  regressions are load-bearing for on-chain correctness. Initial run
  produced 92.6% kill rate with two survivors: (1) a real gap at
  `num < 0` boundary killed by an added test, raising the score to
  **96.3%**; (2) an equivalent mutation at `num > 50` vs `num >= 50`
  that produces the same output on either side of the boundary.
  Threshold configured at high=90 / low=70 / no break-on-fail since
  mutation runs should be a periodic CI signal, not a PR gate.

Audit-followup.md updated with a status table, known-good fork RPC,
and next hook-test targets.

Verification: default suite 212 → 230 tests across 16 files (+fork
file skipped without env). Typecheck + format clean.
The pre-existing `format.yml` was the only CI gate, so none of the
verification stack we built ran on PRs. This adds four workflows that
align the pipeline with the local hooks:

- **verify.yml** — fast PR gate. Parallel jobs run `tsc --noEmit` and
  `vitest run` on every PR and push to main. No secrets required;
  runs in seconds. This is what catches the bugs the old setup didn't.

- **build.yml** — path-filtered `next build` check. Fires only when
  PRs touch src/app/api/**, src/middleware.ts, src/env/**,
  src/actions/**, or next.config.mjs — the categories where typecheck
  can't prove correctness. Populates .env from .env.example because
  the example ships Zod-valid UUID placeholders (and any new env var
  the example doesn't cover will fail this job and force the example
  to stay current with server.ts).

- **fork.yml** — nightly + workflow_dispatch. Installs foundry via
  `foundry-rs/foundry-toolchain@v1`, runs `npm run test:fork` with
  `FORK_RPC_URL` from a repo secret. NOT a PR gate because public-RPC
  flakes would block merges on a third-party's uptime; a genuine
  on-chain encoding regression still surfaces as a failed job and
  notifies the owner.

- **mutation.yml** — weekly + workflow_dispatch. Runs Stryker,
  uploads the HTML report as a workflow artifact (30-day retention).
  Not a PR gate — mutation score is a test-quality signal, not a
  correctness one.

All four use `concurrency.group` to cancel stale runs. Verified
scripts exist; format:check clean.
@vercel

vercel Bot commented Apr 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
fastprotocolapp Ready Ready Preview, Comment Apr 22, 2026 3:33pm

Request Review

The initial build.yml did `cp .env.example .env` and handed it straight to
`next build`. That blew up in CI on `FAST_RPC_API_TOKEN` because Zod's
`.string().min(1).optional()` only treats `undefined` as the optional
case; an empty string is present-but-invalid.

Fix: after copying, `sed -i '/=""$/d' .env` strips any `FOO=""` line so
optional fields revert to undefined. The required fields
(EMAILOCTOPUS_API_KEY, EMAILOCTOPUS_LIST_ID) ship with real UUID
placeholders in the example and survive the filter.

Keeps the "empty string = fill me in" convention in .env.example that
developers rely on.
…line

The prior README listed a src/ tree from before the agentic-repo-design
refactor and had nothing to say about the testing stack or CI beyond
"Working with AI agents" section. Updates:

- Project structure: shows the new src/lib folders (tokens/swap/
  settlement/config/api/analytics), the top-level tests/ mirror, and
  tests/ subfolders (api, fork, hooks, invariants, fixtures).
- Getting Started: lists `cp .env.example .env` so developers get past
  t3-oss validation on first run.
- Scripts table: documents every npm script, including test:fork and
  test:mutation.
- Testing section: describes the nine layers and which command surfaces
  each, plus the "230 tests in default run" headline.
- CI pipeline section: inventory of the five workflows and which ones
  gate PRs vs. run as scheduled canaries.
- Working with AI agents: expanded with the documentation-layer table
  (skills/agent_docs/docs), the three PostToolUse hooks, and a pointer
  to audit-followup.md for outstanding work.
Sets up the pattern for pulling read-only copies of upstream repos
(starting with primev/mev-commit) under .external/ so agents have
persistent, greppable context without MCP round-trips.

- .gitignore: add /.external/ so the vendored clones never land in commits.
- .claude/externals.json: declarative manifest. One entry per external
  — `name`, `origin`, `ref` (tracking main), optional `sparse` paths,
  `freshness` thresholds, and a `purpose` one-liner. The mev-commit
  entry sparse-checks only the 10 paths Fast Protocol App actually
  consumes (contracts/, contracts-abi/, tools/preconf-rpc/{fastswap,
  service, handlers, store, sender, rpcserver, points}, and
  tools/fastswap-miles/). Everything else — p2p/, bridge/,
  external/geth, infrastructure/, testing/ — is deliberately excluded
  per the scope map in agent_docs/external-mev-commit.md.
- agent_docs/external-workspaces.md: brief pointer doc (how sync
  works, why local-clone beats MCP, how to add future externals).
- agent_docs/external-workspaces-plan.md: the full design rationale,
  including the tracking-main vs pinned-SHA decision and the
  /prime + /sync-externals split.
- agent_docs/external-mev-commit.md: scope map for mev-commit
  specifically — every endpoint the app talks to mapped to the
  upstream Go file that implements it, the miles/Fuul flow, and the
  reverse table (each app file → its mev-commit counterpart).

Phase B (sync hook + commands) and Phase C (skill wiring) follow.
…mands

Adds the mechanism that materializes and refreshes everything declared
in .claude/externals.json. Strict semantics throughout: fast-forward
only, never auto-resolve, refuse to proceed on diverged state.

- .claude/hooks/externals-sync.sh: reads the manifest with jq, clones
  with --filter=blob:none + sparse-checkout when patterns are given
  (9MB mev-commit checkout instead of 137MB full clone), fetches +
  fast-forwards existing clones, writes .external/.manifest.lock.json
  with {name, sha, ref, fetchedAt, ageDays} per external. Prints
  machine-parseable per-line status. Sparse patterns use gitignore
  style in the manifest ("/foo/") for human readability and get
  normalized to cone-mode style ("foo") before hand-off to git
  sparse-checkout.
- .claude/hooks/pre-commit-external-guard.sh: rejects staged paths
  under .external/. Catches the real failure case — `git add -f
  .external/` accidentally embedding the clone as a gitlink — not
  just per-file edits. Ships as a script; install with a one-liner
  symlink documented in the README.
- .claude/commands/prime.md: invokes externals-sync.sh before the
  orientation file reads. If the sync exits non-zero, prime surfaces
  the error and stops — no self-repair on .external/ allowed. Adds
  an "External workspaces:" section to the final summary.
- .claude/commands/sync-externals.md: the mid-session refresh knob.
  Runs just the sync, no orientation reread. For when a PR lands
  upstream and you want the new state immediately.

Manually tested: clone + sparse-checkout produces the expected 10
paths (contracts/, contracts-abi/, tools/preconf-rpc/{fastswap,
service, handlers, store, sender, rpcserver, points}, and
tools/fastswap-miles/). Disk footprint 9MB vs 137MB full clone. The
pre-commit guard correctly exits 1 when .external/ is staged.

Regression check: 230 tests pass, typecheck clean.
Skill that tells agents WHEN to load mev-commit context and WHERE to
look. Without this, the vendored mirror exists but agents have no
discovery signal — they'd wander the 9MB tree and burn context.

- SKILL.md: trigger conditions (editing fastswap route, debugging
  preconf status errors, miles discrepancies, ABI drift, revert
  investigation) + hard rules (never write to .external/, check
  freshness before citing) + pointer to the authoritative scope map
  in agent_docs/external-mev-commit.md.
- contracts.md: Solidity source at .external/mev-commit/contracts/.
  Highlights FastSettlementV3.sol + IFastSettlementV3.sol + the
  test/ dir as a reference for how upstream calls the contract.
  Includes a three-step revert-debugging recipe.
- abis.md: canonical ABI JSON at .external/mev-commit/contracts-abi/abi/
  and the drift policy that's enforced by
  tests/contracts-abi/abi-drift.test.ts (extension lands in Phase D).
- protocol-types.md: the preconf-rpc HTTP/JSON-RPC surface — every
  endpoint the app calls mapped to the Go handler — plus the miles
  indexer flow. The SwapRequest struct is embedded inline so agents
  touching src/app/api/fastswap/route.ts can verify shape without
  opening the upstream file.

Each file deliberately thin — the full scope map stays at
agent_docs/external-mev-commit.md so we don't drift two sources.
Closes the loop on the vendored-externals pattern: every local ABI
that has a counterpart in .external/mev-commit/contracts-abi/abi/ is
diffed byte-for-byte (canonicalized JSON) against upstream. Drift
fails the test, giving us a deterministic signal when someone
hand-edited a local ABI without sourcing from upstream.

- tests/contracts-abi/abi-drift.test.ts: new `describe.skipIf(!synced)`
  block that walks every local .abi file, skips when the upstream
  copy is absent (e.g., our IFastSettlementV3.abi is a local-only
  interface extract), and asserts semantic equality otherwise. Sort
  keys + whitespace-canonicalize before compare so formatting
  differences don't flag as drift.
- package.json: `test:externals` (runs just the ABI drift suite) and
  `sync:externals` (convenience alias for the sync hook).
- .github/workflows/externals.yml: weekly + manual. Runs the sync
  hook, then `npm run test:externals`. Writes the lock-file contents
  to the job summary so a reviewer can see the exact upstream SHA
  each run exercised. NOT a PR gate — this is a canary.

Verified locally: 15 drift tests pass, 1 skip (IFastSettlementV3.abi
has no upstream counterpart). Default test suite grows 232 → 235 (the
new tests only fire when .external/ is present, which default runs
don't have).
Splits the README into two clearly signposted halves so it serves both
audiences without making either wade through the other's content:

1. Humans exploring the codebase: top-matter (what it is, tech,
   getting started, scripts), project structure (now reflecting the
   folderized src/lib, top-level tests/, and the new .external/),
   testing (ten layers mapped to which command surfaces each), and
   CI pipeline (all six workflows, which gate PRs vs. run scheduled).

2. AI agents starting a session: a signposted "Working with AI
   agents" section covering /prime, the primitives table
   (CLAUDE.md / AGENTS.md / agent_docs/ / skills / subagents /
   commands / audit-followup), the three-tier doc-layer convention,
   every PostToolUse hook, the external-workspaces pattern (how
   .external/ + mev-commit work), and a one-sentence workflow
   summary.

Opening callout at the top tells each reader where to look so
nobody has to skim for their section.
Two new indexes so agents can answer "does X exist?" with one read
instead of a recursive grep:

- src/app/api/README.md — 52 API routes grouped by domain (analytics,
  barter, config, cron, fastswap, feedback, fuul, gate, OG, tokens,
  user-community-activity, user-onboarding, waitlist, whitelist).
  Each row: path, HTTP methods, one-sentence purpose, Zod-migration
  status (24/52 on the new pattern, 28 flagged ⚠️ for migration
  when touched). Closes with the "add a new route" recipe pointing
  at the next-app-router skill.
- src/hooks/README.md — 50 hooks grouped by concern (swap, permit2,
  wallet/RPC, balances, transactions, dashboard, leaderboard/miles,
  onboarding/gating, SBT, email, utility). Each row: hook + one-sentence
  purpose. Closes with the "add a new hook" recipe including test-file
  convention.

These are hand-maintained for now; drift risk is minimal because
adding to the index is part of the routine when landing a new
route/hook (recipe in each README makes that explicit). A future
generator script could automate from JSDoc comments but isn't
necessary yet.
Single source of truth for the two app-owned tables (user_onboarding,
user_activity). Documents columns, defaults, every route that touches
each table, test fixtures, conventions, and — critically — which
reads use window functions (`ROW_NUMBER() OVER`) that pg-mem can't
run, so agents know why some routes have integration tests and some
don't.

Also a pointer section for the three out-of-scope data surfaces
(StarRocks analytics, Fuul API, Google Sheets) so an agent landing
here has one hop to find the right layer.

Closes the reverse-engineer-from-SQL-every-time gap.
One top-level file listing every "always-true" rule the app upholds,
grouped by domain (API input, slippage math, EIP-712 signing, token
resolution, wrap/unwrap detection, stablecoin detection, leaderboard
pagination, upstream API contracts, ABI drift). Every entry links to
the test that enforces it — the test IS the oracle, this doc is
navigation.

Agents can now load "what are the rules of this system" in one read
instead of grepping property tests. Scroll-optimized: one-sentence
invariants, tables sorted alphabetically within each section.
Adds a scoped no-restricted-syntax rule for src/app/api/**/route.ts
that flags the three imperative-validation patterns still present in
~7 routes:

  - request.json() (→ use parseJson)
  - request.nextUrl.searchParams (→ use parseSearchParams)
  - new URL(request.url) + searchParams.get (→ use parseSearchParams)

Each warning links to .claude/skills/next-app-router/api-routes.md
for the migration recipe.

Warn-level (onlyWarn downgrades anyway) because we don't want to
force-migrate every route in one PR — the goal is to surface the
pattern gap the moment an agent opens one of these files. The
audit-followup doc retains the list of remaining routes for
deliberate catch-up work.

Verified: fires on analytics/leaderboard/{,efficiency-leaders,rising-stars,
volume-leaders}/, analytics/l1-swap-hashes/, fuul/leaderboard/,
user-community-activity/stats/. Clean on all 24 already-migrated
routes.
Turns on the first strictness flag and fixes the modest fallout
(8 initial errors, 5 after @types/pg):

- Install @types/pg so the pg.Pool client isn't an implicit any —
  that alone resolved 3 errors across the community-activity routes.
- src/app/api/gate/status/route.ts: declare GateStatusResponse so
  EMPTY_RESPONSE.position isn't inferred as bare `null` (wire shape
  with the UI is now typed, not conventional).
- src/components/providers.tsx: WalletDisconnectHandler returns null,
  annotate explicitly. Same for src/components/pwa/service-worker-register.tsx.
- src/lib/email.ts: annotate `.catch((): null => null)` so the
  rejection branch has an explicit return type.
- tests/api/user-onboarding.integration.test.ts: type the test-case
  array so `body: undefined` doesn't infer to any.

Next strictness flips (noUnusedLocals, strictNullChecks) are
catalogued in audit-followup.md — each is a separate, reviewable PR
so the blast radius stays small.

Verification: 232 tests green, typecheck clean, format clean.
Restructures the file into three clear sections (Completed /
Outstanding / Lower-priority) and updates to reflect the latest
batch:

- Completed: route + hook discovery indexes, db-schema.md,
  INVARIANTS.md at repo root, ESLint rule for non-Zod routes,
  noImplicitAny: true flipped + fallout fixed, external-workspaces
  pattern (mev-commit wired + drift-check CI).
- Outstanding: further strictness flips in order (noUnusedLocals/
  noUnusedParameters → strictNullChecks + parse helper rewrite →
  full strict), 28 remaining API route migrations (flagged by the
  new ESLint rule), the three god-file splits (now unblocked by
  hook-test pattern), more hook tests, component test pattern,
  pg-mem window-fn limitation + testcontainers path.
- Lower-priority nice-to-haves: /verify-ui, error taxonomy doc, PR
  review automation, Dependabot, perf budget, a11y baseline,
  recent-incidents ledger, widen Stryker scope, extend externals.json.

Adds an explicit priority order at the bottom so the next agent
picks up in the right sequence.
Closes the API Zod migration — all routes that take user input are
now validated through @/lib/api/parse + @/lib/api/schemas. The ESLint
rule reports zero violations across the 52 route handlers.

Routes migrated:

- analytics/leaderboard/route.ts — optional currentUser via
  walletAddressSchema; lower-casing now happens at the boundary.
- analytics/leaderboard/efficiency-leaders/route.ts — sort enum
  (tx_count | txs_per_day | streak), limit/page/tier with defaults.
- analytics/leaderboard/rising-stars/route.ts — sort enum
  (climbers | new_users | wow_growth), limit/page.
- analytics/leaderboard/volume-leaders/route.ts — sort enum
  (volume | avg_size | largest), limit/tier/page.
- analytics/l1-swap-hashes/route.ts — limit coerced + clamped
  [1, 1000] (DoS guard).
- user-community-activity/stats/route.ts — optional entity (trimmed)
  and chainId filters.
- og/preconfirm/route.tsx + [time]/route.tsx — time clamped [0, 999],
  soft-fail to default 0.4 on parse failure (OG crawlers shouldn't
  get a 400).

For the enum-constrained sort routes, the parser rejects unknown
values with a 400 instead of silently falling through to the default
branch — a real safety improvement over the prior imperative code.

Verification: typecheck clean, 232 tests green, ESLint reports zero
no-restricted-syntax violations on the API tree.
…decls

Turns on the two unused-code TypeScript flags and fixes the fallout —
77 TS6133 / TS6192 errors across 39 files. Bulk of the work is
mechanical dead-code removal (unused imports, unused props, unused
destructured variables, unused local variables, unused function
parameters), with a handful of genuinely dead code paths excised.

Tooling:
- Added eslint-plugin-unused-imports to auto-fix unused imports via
  `eslint --fix`. Kept @typescript-eslint/no-unused-vars off since
  tsc now owns that signal — unused-imports only handles the
  auto-fixable import arm.

Patterns applied:
- Unused imports → deleted (eslint-plugin-unused-imports --fix)
- Unused destructured properties → removed from destructuring (not
  renamed to _name, which would create TS2339 for typed sources)
- Unused function parameters → prefixed with `_` (TS convention)
- Unused local variables → declaration deleted
- Unused React prop destructuring → removed (component still accepts
  the prop in its Props type; just doesn't read it)

Notable dead-code removals:
- analytics/leaderboard/route.ts: userChange24h was assigned twice
  but never read in the response. All three lines deleted.
- providers.tsx: refreshPage() helper was defined but only referenced
  in commented-out call sites.
- use-swap-form.ts: useBroadcastGasPrice() was called but gasPriceGwei
  never consumed; removed the call + import.
- use-swap-confirmation.ts: source/target vars computed but never
  passed to the settlement path.
- LeaderboardTable.tsx: second useState for activeTab / setActiveTab
  that shadowed the used one below — the outer was dead.
- Various components: initialEthPrice, initialTotalPoints, points,
  isMounted, onOpenChange, toBalance, etc. — props passed down but
  not consumed. Left the Props types intact; callers still pass them.

Tsc flags remaining off (future PRs):
- strictNullChecks (will require rewriting @/lib/api/parse.ts)
- strict (master flag, gated on the above)

Verification: 232 tests green, tsc --noEmit clean, format clean.
Two items moved from Outstanding to Completed:
- All 52 API routes that take user input now go through @/lib/api/parse
  (was 24/52; the remaining 28 migrated in this session's earlier PR).
- tsconfig now has noUnusedLocals: true + noUnusedParameters: true
  on top of the already-flipped noImplicitAny. 77 dead declarations
  were purged along the way.

Re-ranks the priority list to reflect the new top items:
1. Extract use-swap-form math helpers
2. Seed 2-3 more hook tests
3. Establish the component-test pattern
4. strictNullChecks + @/lib/api/parse rewrite
5. Full god-file splits
6. Dependabot
7. Widen Stryker scope
…t + 14 property tests

Extracts the applied-slippage picker out of the useMemo block in
src/hooks/use-swap-form.ts. That block merged three independent
concerns — user slippage, Barter shortfall + buffer, maxSlippage cap —
into bigint arithmetic that couldn't be exercised without mounting
React. Pulled the percent math into a standalone module; bps + limit
computation now reuses the existing @/lib/swap/slippage helpers.

- src/lib/swap/min-amount-out.ts (new): computeAppliedSlippagePct and
  computeAppliedSlippageBps. Documents the rule in prose (user
  slippage is the floor, Barter shortfall + buffer raises it when
  positive, both capped at maxSlippagePct). Guards NaN / negative
  inputs — the UI can send either while the user is mid-type — so the
  helpers are total.
- src/hooks/use-swap-form.ts: the computedMinAmountOut useMemo now
  calls computeAppliedSlippageBps + computeSlippageLimit. ~12 lines
  of inline math → 3-line call chain. No behavior change.
- tests/lib/swap/min-amount-out.test.ts (new): 5 example tests + 9
  property tests covering:
    - Output always in [0, maxSlippagePct]
    - Never below userSlippagePct (contract-safety: we never silently
      tighten a user's tolerance)
    - Monotone non-decreasing in barterShortfallPct
    - Shortfall=0 collapse semantics
    - DEFAULT_BARTER_BUFFER_PCT pinned to 0.5
    - End-to-end composition with computeSlippageLimit produces a
      floor ≤ amountOut for ALL inputs (the revert-safety property)
    - Totality (never throws)
- INVARIANTS.md: new "Applied-slippage picker" section linked to the
  new tests.

Verification: 232 → 246 tests green, typecheck + format clean.
use-swap-form.ts size essentially unchanged (a few lines saved), but
the math is now independently verifiable.
Adds a new test-pyramid tier for WCAG 2.1 AA sweeps. Components are
rendered under happy-dom exactly like the functional tests, but the
assertion runs axe-core over the produced DOM instead of checking
behavior.

- `tests/a11y/` — own directory so a11y triage doesn't compete with
  behavioral failures in stack traces.
- `tests/utils/axe.ts` — thin wrapper locking the rule set to
  WCAG 2.1 AA plus a `formatViolations` helper that surfaces rule id +
  help URL + offending HTML when a test fails.
- `tests/a11y/SwapToast.a11y.test.tsx` — first canary. Pins three
  visual states of the post-swap toast (pending, confirmed,
  barter-slippage retry card) as the most-visible-per-user surface in
  the product. Mocks are the same as the functional SwapToast test.

No violations today. As god-files continue to split, copy this template
for the critical leaves: swap confirmation modal states,
LeaderboardHeader, AppHeader.

Deps: added `axe-core@4.11.3` + `vitest-axe@0.1.0` as devDependencies.
Suite total: 297 passing (+3).
One-shot UI smoke via the existing ui-verifier subagent. Boots
\`npm run dev\`, curls /, /dashboard, /claim, checks HTTP 200 plus
content markers (LEADERBOARD / Genesis / gate heading), and cleans up
the server on exit.

Intended to catch provider crashes, missing env vars, and blank-page
hydration failures — NOT pixel-level drift. Visual parity after a
refactor still needs a human in a browser.

Also registered the command in CLAUDE.md's slash-commands list.
…hemas/min-amount-out tests

**Security fix (high).** The post-strictNullChecks rewrite of
\`src/app/api/fastswap/route.ts:41\` was spreading the entire
\`ParseResult\` wrapper (\`{ ok, data: {...} }\`) into the upstream
request instead of just \`.data\`. Caller would receive
\`{ ok, data: {…permit2 intent…}, slippage }\` instead of the flat
intent, breaking the FastSwap proxy. Caught by security-reviewer on
the branch diff.

Today no client calls this route (the permit path hits FastRPC
directly), so no user impact — but the next engineer to re-enable the
proxy would ship broken. Tightening this now keeps the fix with the
original rewrite instead of leaving a landmine.

**Stryker kills.** Killed every survivor that a real input could
distinguish:
- \`schemas.ts\`: 72 → **100** mutation score. Added tests for
  (a) trailing-\`$\` anchor on wallet/tx-hash regex (valid prefix + junk
  suffix must reject — caller-controlled suffix is an injection class),
  (b) leading-\`^\` anchor (prefix smuggle),
  (c) explicit error-message assertions ("Invalid wallet address",
  "Invalid transaction hash", "Symbol is required", "Symbol too long")
  so a typo breaks the test instead of rotting silently.
- \`min-amount-out.ts\`: 67 → **81** mutation score. Added
  \`Number.POSITIVE_INFINITY\` / \`NEGATIVE_INFINITY\` cases for both
  \`userSlippagePct\` and \`barterShortfallPct\` so the
  \`Number.isFinite\` guard is visibly load-bearing. A missing guard
  would peg applied slippage at the cap when a NaN/Infinity slips
  in from a downstream division.

Remaining 5 survivors (4 in min-amount-out, 1 in slippage) are
equivalent mutants — both branches produce the same output at
boundary values (e.g. \`num > MAX\` vs \`num >= MAX\` returns the same
value at \`num = MAX\`). Would need a code-shape change to kill.

Overall mutation score: 78.5 → **83.5**. Suite: 308 passing.
The original /verify-ui pointed at /dashboard as the leaderboard route,
but ui-verifier flagged this: the heavy-refactor target is actually
/leaderboard (src/app/(app)/leaderboard/page.tsx). /dashboard is the
personal miles page.

Both routes are now listed so the smoke exercises the full authenticated
header + provider stack plus the LeaderboardTable split itself. Also
documented two operational gotchas that surfaced on the first run:
stale .next/ requires rm -rf, and missing ANALYTICS_DB_AUTH_TOKEN makes
SSR fetches 401 without crashing the page.
First test file for the 600-LoC use-swap-form god-hook. Establishes a
regression oracle the next extractor can lean on — without this, any
further carve (refresh timer, quote cache, switch handler) was blind.

Ten tests pin the parent-surface contracts that matter to the swap
button:
- Defaults: fromToken = ETH, toToken = undefined, amount = "",
  editingSide = "sell", timeLeft = 15.
- Amount input: setAmount updates + preserves across rerender, empty
  string allowed for "user clearing the input".
- Token changes: setToToken updates; pair change clears isManualInversion
  AND swappedQuote (pair-change effect is the load-bearing reset).
- Wallet lifecycle: connected→disconnected edge resets the form;
  connected→connected (account switch) preserves the user's input.
- clearSwapState pulse: setting it true clears amount/editingSide/
  inversion flags but leaves the token pair intact so the user can
  immediately do another swap in the same direction.

Notable gotcha documented in the test file: the pairKey-change effect
fires AFTER batched setState inside the same `act`, so tests that need
to set `isManualInversion = true` must split the token-change and the
flag-set into separate `act` blocks. This is the exact shape of bug
future extractors will introduce if the effect ordering drifts.

Ten external dependencies mocked at module scope (wagmi, TanStack Query,
and eight internal hooks). wagmi exposes a `__setAccountForTest` mutator
so specific tests can flip wallet state mid-run without re-mocking.

Suite: 315 passing (+10).
Eighteen leaves were extracted this session from LeaderboardTable (11)
and SwapConfirmationModal (7) with zero direct tests. I claimed they
were "now independently testable" — this commit starts paying that bill.

Three new test files, nineteen tests. Targets chosen for load-bearing
behavior, not LoC:

1. **useSnapshotOnOpen** (5 tests) — contract-critical hook that freezes
   quote values on modal open. If it leaks live updates, the user signs
   a tx with different numbers than the ones they reviewed. Pins the
   closed→open capture edge, the close clears the snapshot, re-open
   re-captures, and identity stability across open=true renders (a
   regression that re-spreads on every render would let live updates
   drift through cumulatively).

2. **ErrorView** (7 tests) — barter-slippage retry path is money-critical.
   Pins: header copy, the retry button's parsed slippage %, the clicked
   callback argument is a string (not a number — it must flow through
   the swap form's slippage string store), Details button wiring, the
   occurredAfterPreConfirm branch that HIDES Try Again (otherwise retry
   would submit a second tx for an already-preconfirmed intent).

3. **LeaderboardHeader** (7 tests) — the top-of-page mode toggle that
   drives which mode-table below it even renders. Pins: toggle callback
   routing, Traders / Vol (ETH) / Vol (USD) labels + values, '---'
   fallbacks for null stats (SSR / unauth path), user performance card
   visibility gated on userAddr, '#--' placeholder for in-flight rank.

Pattern follows the SwapToast + use-swap-form templates already in the
tree — happy-dom, renderHook/render, @testing-library queries, vi.fn()
spies for callbacks.

Not yet covered (biggest remaining holes): VolumeModeTable (tier filter
+ user-position divider logic), MilesModeTable, VolumeProgressAnalysis.
These are the next natural additions when touched again.

Suite: 334 passing (+19).
…lose button

Closes the biggest untested surface on this branch. The useSnapshotOnOpen
hook and the ErrorView / ConfirmCtaButton leaves each have their own
tests, but the orchestration BETWEEN them — which is what the 623-LoC
post-split file owns — was not directly tested.

Eight tests covering load-bearing orchestration contracts:

1. autoExecute short-circuit — open + autoExecute renders null AND fires
   executeSwap AND calls onAutoExecuteConsumed. This is the toast-retry
   fast path; a regression that shows the review UI would let the user
   re-review a swap they already confirmed.

2. CTA routes to the right primitive — three separate tests pinning
   wrap() / unwrap() / confirmSwap() so a future refactor can't shuffle
   the call-site wiring. Each verifies the "other two" are NOT called.

3. Approval flow — needsPermit2Approval=true + intentPath surfaces
   "Approve & Swap" and routes the click to onApprove, NOT confirmSwap.
   The swap auto-chains on a subsequent render when needsPermit2Approval
   flips false; that edge is driven by props and lives in an effect that
   would need wall-clock re-renders to exercise — documented but not
   asserted.

4. External error — externalError prop renders ErrorView, hides review.
   occurredAfterPreConfirm hides Try Again (tx already on L1; retrying
   would submit a second tx).

5. Close handler — clears lastTxError on the store and fires
   onOpenChange(false).

**a11y fix:** the modal's close button had no accessible name (the
Radix DialogClose wraps a bare button with just an X svg). Added
\`aria-label="Close"\` + \`type="button"\`. This is a real WCAG 2.1 AA
regression, not just test theatre — screen readers could not
announce the close action before.

Ten external hooks mocked (wagmi, useWethWrapUnwrap, useSwapConfirmation,
plus gas/price/miles utilities). Tests use a mutator pattern
(mockIsWrap / mockIsUnwrap module-scoped booleans) so a single test
can flip the mode without re-mocking the module.

Suite: 342 passing (+8).
@passandscore passandscore changed the title Agent-first repo: folderize, Zod, 9 test layers, CI pipeline Agent-first repo: strict TypeScript, god-file splits, 10 test layers, CI pipeline Apr 20, 2026
Main added commit 8743544 ("feat: add /pro landing + fix OG image
pre-warm URL") which:
- Added a /pro landing route (src/app/pro/*, src/components/pro/*).
- Loosened early-access validation to "at least one contact method
  required" (was all three).
- Deleted src/app/share/preconfirm/route.ts as a dead route.
- Fixed the OG pre-warm URL in SwapToast.

Conflict resolved in src/app/api/early-access/route.ts: kept this
branch's Zod-migration shape but adopted main's semantics. The
earlyAccessSchema now marks x_handle / discord_handle / email as
optional-defaulting-to-empty and uses a top-level .refine to require
at least one. Email-format check stays in the schema (fires only when
email is non-empty).

Also updated src/app/pro/layout.tsx to import SITE_URL from
@/lib/config/site (the new path after this branch's src/lib/
folderization) instead of the old @/lib/site-config.

Merge preserves: main's new /pro route, loosened waitlist entry rules,
corrected OG pre-warm URL, deleted dead share route. Branch preserves:
Zod-through-parseJson pattern, ParseResult discriminated union, all
god-file splits and strictNullChecks fallout.

Verified: typecheck clean (filtering known stale .next/types noise),
342 tests pass (+3 skipped), format clean.
… with folderization

Durable flow for keeping the agentic-repo patterns aligned as main moves.

**New skill: `.claude/skills/merging-main/SKILL.md`**
Playbook for merging / rebasing main and catching drift. Covers:
- Stale `src/lib/*` imports (the pre-folderization → post-folderization
  table: site-config, network, feature-flags, weth-abi, weth-utils,
  erc20-abi, constants, stablecoins, token-list, token-resolver,
  transaction-errors, slippage, quote-guard, eth-path-tx, permit2).
- ESLint `no-restricted-syntax` hits on new API routes — main PRs that
  opened before the rule can reintroduce imperative validation.
- New `any` / `@ts-ignore` added since the merge base.
- Doc-index drift (architecture.md, api/README.md, hooks/README.md).
- Test seeds and a11y sweep on user-facing new components.
- Bundle check for new runtime deps.

**New slash command: `/realign`**
Runs the skill's playbook step-by-step. Reports incoming commits, drift
caught, docs updated, verify results, and anything flagged but not
auto-fixed. Returns the report without committing so the human can review
before the merge commit is finalized.

**Live pass over PR #109 (/pro landing) merge** — caught three issues:

1. `src/app/pro/layout.tsx` imported `@/lib/site-config` (main's path);
   fixed to `@/lib/config/site` during the merge commit b810514.
2. `agent_docs/architecture.md` was missing entries for the new
   `src/app/pro/` and `src/components/pro/` folders (and still listed
   the deleted `src/app/share/` route) — fixed here.
3. `src/app/api/fuul/leaderboard/route.ts:200` uses `new URL(request.url)`
   in an error-path salvage fallback that the security-reviewer cleared.
   Added `eslint-disable-next-line no-restricted-syntax` with a comment
   explaining why — prevents the warning from rotting into noise.

Also documented in `agent_docs/audit-followup.md`:
- The merging-main flow landed (move to "done" section).
- The `PaginatedLeaderboardModal` `(e as any)` casts as a known cleanup
  (genericize the modal over `T extends PaginatedModalEntry`). Pre-
  existing from main's `e57a5f8`; not a merge regression.

Verified: 342 tests pass, typecheck clean, format clean.
…s rule

Highest-ROI drift catcher: every pre-folderization \`src/lib/*\` path is
now an ESLint error with a pointer to the new location. Main PRs that
merge in with stale imports get caught at lint time (seconds) instead
of at \`next build\` (minutes), and agents no longer have to remember
to run \`/realign\`.

## What

`eslint.config.js` gains a `no-restricted-imports` block with 31 old →
new entries covering every file moved by commit 6889c3b ("folderize
lib"):

- **config/** — site-config, network-config, feature-flags, constants,
  leaderboard-config.
- **tokens/** — weth-abi, erc20-abi, token-list, token-resolver,
  stablecoins, stablecoin-list, weth-utils, token-icons, popular-tokens,
  barter-supported-tokens.
- **settlement/** — transaction-errors, transaction-receipt-utils,
  tx-config, fast-rpc-status (→ rpc-status), fast-tx-status (→ tx-status),
  fast-db (→ db), preconfirm-sound.
- **swap/** — slippage, quote-guard, eth-path-tx, permit2-utils,
  barter-api, swap-constants (→ constants), swap-events (→ events),
  swap-server (→ server).
- **deleted** — fast-settlement-v2-1, fast-settlement-v3-abi (removed
  entirely; point users at contracts-abi/).

Every message includes the new path so the fix is a one-line edit.

## Guarded by a test

`tests/infra/eslint-no-restricted-imports.test.ts` — 11 tests spawning
ESLint programmatically. If someone removes an entry or the rule itself,
the test fails. Cases include:

- 9 parametrized stale-path assertions (message contains both old and
  new path).
- Explicit "Removed" check for the two deleted modules.
- Happy-path sanity: post-folderization paths lint silently.

## Skill + command updates

`.claude/skills/merging-main/SKILL.md` — rename table fixed (permit2
was mistyped as lacking the `-utils` suffix; several entries were
missing). Section now reads "handled automatically" with a pointer to
the lint rule. Manual grep commands deleted — the rule replaces them.

`.claude/commands/realign.md` — "Hunt stale lib imports" step points at
`npm run lint` instead of a grep. Also notes that new renames need to
be added to BOTH the lint rule and the skill's table.

## Why this is the right tool

- **Instant feedback** — ESLint reports in seconds; `next build`
  reports in minutes.
- **Better error** — the message names the new path. `next build`
  would just say "module not found."
- **Can't be forgotten** — runs on every lint invocation, including
  the `npm run verify` slash command and CI.
- **Zero runtime cost** — no new deps, pure config.

Verified: 353 tests pass (+11), typecheck clean, format clean, 0 stale
imports in the current tree.
Main landed PR #128 (commit bb87440) renaming the env var:
FAST_RPC_API_TOKEN → FAST_RPC_DB_AUTH_TOKEN. Touches .env.example,
src/env/server.ts, and two API routes that read the token.

Conflicts in two routes, both the same shape: my branch has the
Zod-migrated handler body; main has the same imperative handler but
with the renamed token. Resolution: keep the Zod structure (parseParams
+ parsed.data already does validation), adopt the new token name.
The imperative validation main still had is obsolete in our tree.

Verified: 353 tests pass, typecheck clean (real errors; .next/types
noise filtered), format clean, 0 no-restricted-imports warnings (the
drift rule landed in f071803 caught no false positives on this merge).
Four tasks executed end-to-end; this is the state the agentic-first
effort was targeting. Detail below; summary numbers: 353 → **399 tests**
(+46), mutation score 83.5 → **96.5%**, 3 weak modules lifted to strong,
CI now gates drift, two real a11y/render-loop bugs fixed.

### #49 — Security-reviewer nits

- **`useSnapshotOnOpen` deep-clone.** Swapped the shallow spread for
  `structuredClone`. Locked with a new test that mutates a nested Token
  object after capture and asserts the snapshot stays immune. The
  security-reviewer's "shallow spread could leak mutations" finding is
  now gone; a regression (reverting to spread) would fail the test.
- **SwapToast render-body console.log.** Moved the Hash-ready log into
  a `useEffect([effectiveHash])`. Previously ran during render — React
  strict mode would double-fire it and the ref mutation was a during-
  render side effect. Log behavior is unchanged (one line per unique
  hash).

### #50 — CI workflow for drift catching

- **`verify.yml` gains a `lint` job** that runs `npm run lint` then
  inspects the output for `no-restricted-imports` (pre-folderization
  paths) and `no-restricted-syntax` (imperative API validation) hits.
  The `onlyWarn` plugin downgrades everything to warnings, so the
  bash post-check is what actually fails CI on drift — a PR from main
  that reintroduces either anti-pattern gets blocked at review time.
- Error output uses `::error::` annotations so GitHub surfaces them
  inline on the PR.

### #51 — token-resolver.ts mutation score

- **78% → 98.4%** (21 survivors → 2 equivalent-only).
- Added 13 targeted tests covering: object-with-no-address branch,
  missing-symbol safety, ZERO_ADDRESS lookup rewrite, crowded-list
  matching (kills the `.find(() => true)` mutant in both
  resolveTokenAddress and resolveTokenDecimals), non-string/non-object
  fallthrough, non-number decimals default, isNativeETH edge inputs,
  getTokenSymbol primitive safety.
- Remaining 2 survivors are `typeof === "object" && X.field` vs
  `true && X.field` — equivalent for non-objects (X.field is always
  undefined-falsy), unkillable without reshaping the source.

### #52 — Component tests on extracted leaves

Four new files covering load-bearing leaves:
- **BuyReceiveValue** (6 tests) — NumberFlow wrapper; numeric vs.
  string fallback; comma-strip for grouping separators.
- **ConfirmCtaButton** (8 tests) — disabled / dangerous / default
  style precedence; native `disabled` attribute gates clicks; spinner
  wrapper toggle.
- **ErrorDetailModal** (5 tests) — Copy-to-clipboard via stubbed
  `navigator.clipboard`; 2000ms "Copied" → "Copy" label flip with
  fake timers; no-op on empty content.
- **LeaderboardRow** (13 tests) — zero-padded rank display; tier
  accent gated on rank ≤ 3 (not just tier); YOU badge via both
  `isCurrentUser` and `showYouBadge`; swap-count plural/singular/NA;
  miles formatter fallback.

### What was NOT done (and why)

- **VolumeModeTable / MilesModeTable** — larger components; their
  logic is partially exercised via LeaderboardHeader + LeaderboardRow
  + PaginatedLeaderboardModal's parent. Natural to add when they're
  next touched; not load-bearing enough to block "done."
- **InfoRow, TransactionSummary, SwapDetailsCollapse, *LeadersCard
  siblings** — pure presentational / indirectly exercised / heavy
  mocking requirements for marginal coverage gain.

Verified: 399 tests pass, typecheck clean (real errors; .next/types
stale-build noise filtered), format clean, 0 stale imports in the
current tree. audit-followup.md updated to reflect the remaining
work as maintenance / judgment items, not completion blockers.
…erge

Local auto-run for the `merging-main` alignment playbook. The hook
fires after `git merge` / `git pull` completes and surfaces drift
within seconds, so you catch it before the next push rather than
waiting for CI.

## Mechanics

- `.husky/post-merge` runs `npm run lint` and greps for
  `no-restricted-imports` (pre-folderization paths) and
  `no-restricted-syntax` (imperative API validation) hits.
- Skips silently on merges that don't touch `src/` (pure doc/config).
- Skips when `GITHUB_ACTIONS=1` or `FAST_SKIP_DRIFT_HOOK=1` (for CI
  and one-off overrides).
- Always exits 0 — the merge is already committed by the time this
  fires, so blocking via non-zero only confuses the user into
  thinking the merge itself failed. The loud warning is the signal.
- Points at `.claude/skills/merging-main/SKILL.md` and `/realign`
  for fixing, mirrors the CI `lint` job so local = CI signal.

## Install path

- `package.json` gets `"prepare": "husky"` + husky as a devDep.
  Next developer to run `npm install` gets `core.hooksPath=.husky/_`
  wired automatically.
- `.gitignore` excludes the auto-regenerated `.husky/_/` dispatcher
  but commits the user-defined `.husky/post-merge` script.
- Default `.husky/pre-commit` that husky init writes was deleted —
  I didn't want `npm test` slowing every commit.

## Docs

- CLAUDE.md's Hooks section adds the post-merge entry alongside the
  PostToolUse + Stop hooks.
- `.claude/skills/merging-main/SKILL.md` now calls out the three
  catch points (local post-merge, CI lint, manual `/realign`).

Verified: hook fires and reports no drift on the current tree; all
399 tests still pass, typecheck + format clean.
Main PR #129 landed two commits:
- Dedicated `show_miles_in_swap` feature flag (separate from the broader
  show_miles_estimate used across the leaderboard).
- New `/api/config/global-banner` route + `GlobalBanner` component for
  Edge-Config-driven announcements rendered at the app shell level.

No conflicts. `/realign` checks clean on the merged tree:
- 0 pre-folderization imports (no-restricted-imports rule silent).
- 0 imperative validation hits (new GET route takes no input, Zod not
  applicable; marked ⚠️ in the route index).
- 0 new `any` / `@ts-ignore`.

Doc update: added `config/global-banner/` to the api/README.md index
(prettier-formatted).

Verified: 399 tests pass, typecheck + format clean.
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