Skip to content

Sync upstream v2.12.1 (merge conflicts) - #33

Open
JOY (JOY) wants to merge 87 commits into
mainfrom
sync-upstream-v2.12.1
Open

JOY (JOY) wants to merge 87 commits into
mainfrom
sync-upstream-v2.12.1

Conversation

@JOY

Copy link
Copy Markdown

Upstream Sync - v2.12.1

Auto-merge with upstream v2.12.1 failed due to conflicts.

To resolve:

  1. Check out this branch locally
  2. Resolve conflicts
  3. Push and merge this PR
  4. Then create tag v2.12.1 to trigger Docker build

Upstream release notes

tom goriunov (tom2drum) and others added 30 commits August 25, 2026 17:42
…3660)

* Unify text truncation behind a single `Truncate` primitive

Replace the four scattered truncation components (HashStringShorten,
HashStringShortenDynamic, TruncatedText, TruncatedTextTooltip) with one
public toolkit primitive, `Truncate`, that dispatches on a `type` prop to
three internal technique modules:

- `middle` (default) — measured middle-ellipsis (container width, binary search)
- `middle-static`    — fixed char count, pure string slice, SSR-safe, cheapest
- `end`              — measured end-ellipsis (CSS text-overflow)

App code now picks *what* (`value`) and *how* (`type`); implementation
details stay internal. `TruncatedTextTooltip` becomes the toolkit-internal
`OverflowTooltip` primitive (still used directly by tag/badge, which wrap
arbitrary children). `shorten-string` moves into the truncation dir.

The entity hub keeps its `truncation` prop and maps to `Truncate` in one
place, so the 115 entity call-sites are untouched. Its tooltip config is
memoized to preserve the leaf `React.memo` in table rows.

Tooltip contract is now uniform: shown only when the value is truncated;
`tooltip.content` overrides content (not the trigger); `tooltip={false}`
suppresses it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Address review on the Truncate primitive

- Force the tooltip when it carries actionable UI: add `always` to the
  tooltip config, honored by all three techniques, and set it wherever
  `tooltipContentAfter` is present so the advanced-filter link stays
  reachable on wide viewports (F1).
- Detect truncation by content, not length, in TruncateMiddle (F2).
- Name-export the three technique modules per the toolkit convention (F3).
- Move `shorten-string` to `toolkit/utils/texts`, name-export it, and
  re-export from the package index; update all importers (F4).
- Fix the stale fontWeight-observer comment and drop a reviewer-facing
  comment (F6, F7).

Also align OverflowTooltip's props with Chakra's Tooltip (content/positioning).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Fix end-truncation layout and tooltip regressions

`Truncate type="end"` (TruncateEnd) was the only technique module missing
`asChild` on its Skeleton, so the non-loading fast path wrapped the text in
an extra `div`. Two visual regressions followed:

- TokenEntity: OverflowTooltip measured the wrapper div instead of the
  clipping span, so a truncated symbol reported not-truncated and its
  tooltip never opened. Add `asChild` so the text lives in the single
  clipping/measured span, matching TruncateMiddle/MiddleStatic.
- TokenInventory: the ID# row (previously a block <Link>) now wraps the
  inline-block Truncate, adding the baseline-descender gap and growing the
  row ~5px. Render that Truncate as `display="block"`, as before.

Verified in Docker against the CI baselines: both target tests plus a
53-test end/tail-truncation sweep pass, no screenshots updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* pass all tooltip props into underlaying components

* fix toolkit build

* fix tests

* [skip ci] pw setup fixes

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* Add a Slack reply watcher to the product-task workflow

New slack-watch skill holds a Socket Mode socket and prints one line per
new reply in a watched thread; grill-the-task Step 3 launches it and drives
each question to settled; questions gain a Resolved-when acceptance criterion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Cover channel threads too, via a bot token

Add a third Keychain token (bot). Route catch-up reads by channel prefix —
user token for DMs, bot token for channels/groups it was invited to — and
ignore both our identities (developer and Honk) when deciding what is a new
reply. One Socket Mode connection carries the app's bot and user events.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Resolve review findings on the Slack watcher (F1–F4)

F1: name both runtimes in slack-watch — Claude Code (Monitor) and Cursor
(background Shell + notify_on_output), mapping launch/stop/escalate to each.
F2: qualify grill-the-task's "never send unreviewed outreach" absolute to
"new" outreach, exempting the already-approved in-thread follow-ups.
F3: state the `Resolved when:` criterion is session-held before to-spec, and
split a sufficient answer's landing spot by task progress (questions.md once
to-spec has run, else held in session).
F4: retry the whole slack-watch init on a thrown startup auth.test, so the
socket never opens with an empty ownIds and reports our own posts as NEW.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…lds (blockscout#3664)

* Add spec for tx details "to" & "value" UI/UX changes (blockscout#3661)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Restyle token-transfer "View all" link on tx details

Remove the leading navigation/tokens icon before the "View all" link in
the token-transfer detail rows and apply the secondary link style. The
overflow show-condition and Token-transfers-tab target are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Surface batch recipients in tx details "To" and "Value"

For an Eden sponsored (batch) transaction with more than one unique
recipient, the "To" field now lists the distinct recipients (first row
rich, rows 2+ bare address, capped at 5 with a "View all (N)" link) and
the "Value" field appends a "to N recipients" link. Recipients are
de-duplicated by address; both links expand the Details section.

Extract the "To" field into TxDetailsTo, derive the breakdown in a
shared getBatchRecipients helper, and add an endContent slot to
DetailedInfoNativeCoinValue for the "Value" link.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Prune task working files for tx details to/value task

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Fix batch-recipient counting and rendering in tx details "To"

Exclude contract-creation calls (null `to`) from the unique-recipient
count so "N recipients" / "View all (N)" reflect addresses only (FR-6),
and render the recipient list by matching the top-level `to` by hash
instead of assuming it is the first call — keeping the shown rows a
subset of the counted recipients in every case.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Update styles for "In/Out/Self" badges

Resolves blockscout#3652

* update screenshots
* Main page stats failover improvements

Resolves blockscout#3655

* review fixes

* correct the data guard and make widgets data source independent from each other

* more review fixes
* Add spec for cyclomatic-complexity and CRAP-score CI gate (blockscout#3663)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Break blockscout#3663 CI-gate spec into tickets

Add the four-ticket decomposition for the complexity/CRAP CI gate:
complexity CLI (tracer bullet), CRAP + coverage/JSX, CI wiring, and
threshold calibration. Amend the spec to drop the third-party service
from the shipped tool and add focused mode + in-tool threshold config.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Add complexity CLI with diff-scoped cyclomatic gate

Introduce tools/code-complexity/: a CLI that computes per-function cyclomatic
complexity by walking the TS/TSX AST (ts.createSourceFile, no type-checker),
matching ESLint's complexity rule. It gates the raw complexity of functions a
diff touches against a configurable cap, prints a table of all checked functions
sorted by complexity, and exits non-zero on any violation. A focused mode scores
every function in explicit file paths, bypassing diff-scoping.

Wired to `pnpm test:code-complexity` via a compile-on-run wrapper (dist/ is
git-ignored). Coverage, CRAP, and JSX handling arrive in later tickets.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Add coverage-aware CRAP gate with self-generating coverage

Extend tools/code-complexity with the second, coverage-aware gate and make
coverage self-service so the tool is usable in one command locally and in CI.

- CRAP: AST-based JSX detection, per-function line coverage joined from a
  v8/istanbul coverage-final.json, CRAP = c²·(1−cov)³+c, second configurable
  threshold. Report gains COV/CRAP/BROKE columns sorted by CRAP descending.
- Coverage sourcing, two axes mirroring vitest. Selection: full-repo (default),
  focused (paths), diff (--changed). Source: generated by running vitest scoped
  to the selection (default), --coverage-file to consume a prebuilt report (the
  CI path, skips vitest), or --no-coverage. Vitest output hidden unless
  --verbose; a no-test or failing run degrades gracefully.
- The CRAP half applies to JSX-less logic files and to JSX components with a
  co-located vitest spec; visual-only components stay complexity-only.
  *.primed.spec.tsx are excluded from coverage runs (page mounts, no behavior),
  and vitest is skipped entirely when nothing in the selection needs coverage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Wire the complexity/CRAP gate into CI with GitHub annotations

The vitest_tests PR run now emits coverage-final.json, and a diff-scoped
post-step gates on it; under $GITHUB_ACTIONS the tool prints ::error
annotations for offenders and writes the report table to the job summary.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Calibrate complexity/CRAP thresholds and stop counting optional chaining

Run the tool repo-wide against whole-suite coverage to produce the complexity
and CRAP distributions, then set the caps from the far tail. Drop optional
chaining `?.` from the cyclomatic count: TypeScript already proves each site's
nullability so no test covers it, and it was the largest source of tail
inflation, dominated by JSX components full of defensive prop reads. Recorded
as ADR 0004; CONTEXT.md points to it rather than restating the rationale, and
the durable docs carry no threshold numbers (which will be re-tuned) or
references to disposable tickets.

Also records ticket 03's human-confirmation checkboxes, left uncommitted after
the CI-wiring commit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Scope blockscout#3663 ticket 05: per-kind complexity caps + per-function CRAP

Turn the brief into a spec: one per-function "directly contains JSX"
classifier drives two complexity caps (jsx backstop / behavior) and moves
CRAP applicability from file-level to per-function (behavior functions only).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Split blockscout#3663 complexity gate into per-kind caps + per-function CRAP

Classify each function by whether JSX appears directly in its own body:
`jsx` render bodies get a high complexity backstop and no CRAP; `behavior`
functions (handlers, hooks, utils) get a tighter cap and the CRAP gate.
This replaces the single complexity cap and the file-level CRAP-applicability
rule with per-function routing.

- complexity.ts: emit containsJsx per function (innermost-frame marking);
  export JSX_KINDS, reused by jsx.ts
- config.ts: DEFAULT_MAX_COMPLEXITY_JSX 30 / _BEHAVIOR 12 / CRAP 80,
  calibrated from a fresh full-repo run (?. not counted, ADR 0004)
- report.ts: FunctionKind, maxComplexityFor, Thresholds shape, KIND column
- analyze.ts: per-function cap selection + CRAP on behavior functions only
- index.ts: --max-complexity-jsx / --max-complexity-behavior flags (bare
  --max-complexity sets both); coverageApplies -> fileNeedsCoverage
  (generation scope only)
- github.ts: kind-aware complexity cap in annotations
- CONTEXT.md: per-function rule for both gates; notes.md records calibration

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Scope blockscout#3663 ticket 06: cognitive-complexity readability cap

Replaces the stopgap cyclomatic caps with a Cognitive Complexity cap as the
sole readability gate (per-kind, both classes); cyclomatic survives only as
the CRAP input. Six agent leaves, one human cap sign-off.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Replace the blockscout#3663 complexity gate with a Cognitive Complexity readability cap

Cyclomatic complexity was a poor "should a human decompose this?" signal — it
scored flat switches and wide if-ladders the same as deeply-nested logic. Swap
the readability gate to Cognitive Complexity (SonarSource model): flat control
flow scores cheap, nesting is penalised progressively, boolean runs collapse.

Cyclomatic complexity stays as the CRAP input and behind --verbose; the
--max-complexity* flags and both cyclomatic caps are retired in favour of
per-kind --max-cognitive[-jsx|-behavior] caps. On a COG violation the CLI emits
an actionable annotation naming the top increment sites, the deepest-nesting
pocket, and the estimated reduction from flattening it.

Caps calibrated from a fresh full-repo CC run (behavior 14, jsx 25) — net-looser
than the retired cyclomatic cap while still tripping the deeply-nested tail.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Register blockscout#3663 follow-up tickets 07 (CC calibration) and 08 (gate outside src)

Ticket 07 is deferred with a brief and a research handoff of the ticket-06
calibration findings — the distribution, the constraints that pinned the current
caps, and the open questions for pushing them further. Ticket 08 is registered
unscoped: extend the CC & CRAP gate to code outside src/ (e.g. tools/).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Scope blockscout#3663 ticket 07: quadratic nesting + ?? exclusion, cap 20

Turn the deferred CC-calibration ticket into an implementable spec. The
investigation resolved to two model changes — exclude null-coalescing from
CC (SonarSource conformance, extends ADR 0004) and penalise nesting
quadratically (1+n², a deliberate divergence) — plus recalibrating the
behavior cap 14 -> 20 at the resulting natural gap. Appends the session-2
decision and reproducible calibration targets to research.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Penalise nesting quadratically in cognitive complexity, exclude `??`

Two coupled changes to the CC readability model, so the gate isolates nesting
instead of punishing breadth:

- Nesting structures charge `1 + n²` instead of `1 + n`. Identical at depths 0
  and 1, biting only from the third level in, so flat and shallow code is
  untouched while genuinely-nested code accelerates away. A deliberate
  divergence from SonarSource.
- `??` / `??=` no longer increment. The SonarSource paper ignores
  null-coalescing as readable shorthand, in the same class as `?.`, so counting
  it was a conformance bug that inflated pure-defaulting functions.

Recalibrated the per-kind caps on a fresh full-repo run: behavior 14 → 20 (the
natural gap between the flat-breadth band and the nested band, firing on 8), jsx
re-confirmed at 25. Together they fire on 24 vs the retired cyclomatic gate's 43.

The flattening-saving estimate in the GitHub annotation is corrected to
`2·depth − 1` per structure, which the linear model had hardcoded as 1.

ADR 0005 records the divergence and its coupling to the caps.

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

* Scope blockscout#3663 ticket 08: extend the CC & CRAP gate to tools/

Allowlist src/ + tools/ over five extensions; deploy/**, test support and
repo-root files stay out with stated reasons. Fixes the two offenders the
gate finds in its own implementation; the tools/ .mjs scripts are deferred
to blockscout#3674 and deploy/tools/'s lint + type-check gap to blockscout#3675.

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

* Extend the CC & CRAP gate to tools/, and fix its own two offenders

The scope rule becomes an allowlist of src/ and tools/ over five extensions,
rather than a src/ prefix test that let everything else fall out by accident.
deploy/** stays out until it is under ESLint and tsc (blockscout#3675); test support and
configuration stay out on the same grounds specs do.

Measured against the existing caps: tools/ adds 185 behavior functions and no
jsx ones, and the 132 in TypeScript top out at exactly CC 20 with none over, so
the caps stand unchanged. Only the three .mjs scripts break (blockscout#3674).

The gate now scores its own implementation, which cost two functions:
complexity.ts's `visit` (COG 31) loses its if-chain pocket to a `visitIfStatement`
helper, and index.ts's `parseArgs` (CRAP 240) becomes a cursor walk over a
discriminated-union flag table, covered by a new index.spec.ts. That table also
removes the prefix-shadowing hazard the old startsWith chain only survived by
arm order.

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

* Restructure the code-complexity docs, move its ADRs into the tool

CONTEXT.md had grown to 285 lines and restated the same rationale in up to
five places: the jsx/no-CRAP argument, the quadratic-nesting argument and the
missing-coverage rule each appeared across the doc, config.ts and three or
four module headers. Paraphrase duplication drifts silently, and it already
had — crap.ts documented a CRAP threshold of 30 when the cap is 80, and the
scope exclusions were "three categories" in scope.ts but "two" in CONTEXT.md.

Give each meaning one owner:

- CONTEXT.md (285 -> 158) is the hub: what the gate is, a new "the gate
  failed — what now" triage section, function classes, modes, scope.
- SCORING.md holds the counting model, with a worked example verified against
  the tool, and the divergences from SonarSource and ESLint.
- CALIBRATION.md holds the caps, the distribution behind them, and a re-tune
  procedure that did not exist anywhere before.
- config.ts (49 -> 18) keeps the numbers and drops the calibration history.
- Module headers state what the file does plus local gotchas; rationale that
  has an owner doc becomes a one-line pointer.

The flag list comes out of CONTEXT.md in favour of --help, which already
covers every flag.

The two ADRs move to tools/code-complexity/adr/, renumbered 0001/0002 — they
constrain only this tool, so a reader working elsewhere never needs them.
AGENTS.md gains the local-ADR convention (numbered per folder, repo-wide
index unchanged, vacated numbers not reused), and its per-directory pointer
now leads with cognitive rather than cyclomatic complexity.

Also drops 13 spec/ticket references from the source: implementation
traceability that resolves to nothing for a later reader.

The doc-link checker only walks files named CONTEXT.md, so the new siblings
are unchecked until blockscout#3674, whose scope now covers extending it.

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

* update ADR section in AGENTS.md

* Prune task working files for the code-complexity CI gate task

The spec still described the cyclomatic-first gate that the first four tickets
built. Tickets 05-08 made cognitive complexity the readability gate, demoted
cyclomatic to a CRAP input, split the cognitive cap per function class and moved
CRAP eligibility from files to functions - none of which the spec absorbed. Since
the FRs are the contract the whole-task review reads, bring them to the shipped
model and drop the points that were only restating the implementation (the doc
split, the increment table, the report columns).

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

* [skip ci] add CC and CRAP to rules

* Resolve review findings on the complexity/CRAP gate

F1 — a skipped vitest run no longer disables the CRAP gate. `resolveCoverage`
returned null when nothing in the selection needed coverage generated, and
analyze.ts reads null as `--no-coverage`. Generation scope thereby decided which
functions were scored (contradicting FR5), and a function's score depended on
what else shared its selection. Skipping the run now yields empty coverage;
only `--no-coverage` yields null.

F2 — `--coverage-file` tolerates a missing report. CI's
`vitest --changed --passWithNoTests` writes no coverage-final.json when it
selects no specs, so the gate step died on ENOENT instead of scoring the
untested code at 0%. Both readers now share `readCoverageOrEmpty`.

F3 — stop ignoring `tools/**` in the Checks workflow. `tools/` is in the gate's
file scope so it scores its own implementation, but a tools-only PR skipped the
workflow entirely and was never gated, linted or type-checked.

F4 — move the ADR mechanics out of always-loaded context into
`.agents/rules/adr.md`, leaving AGENTS.md the locations, the index and a
pointer. Restores the instruction to add a line to that index.

F5, F6 — drop a diff-narrating test comment; point both ADRs at SCORING.md,
which owns the counting conventions CONTEXT.md only links to.

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

* [skip ci] typo fix

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* Enforce react/jsx-filename-extension to keep JSX in .tsx files

Enable react/jsx-filename-extension with { allow: 'as-needed',
extensions: ['.tsx'] } so .tsx files must contain JSX and JSX-free
logic/hook files use .ts. Rename 84 JSX-free .tsx files to .ts.

The rule is syntactic-only (~0.1% of lint rule time), so it has no
measurable impact on lint speed.

Closes blockscout#3668

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Update doc references to renamed .ts files

playwright/lib and useNavItems moved from .tsx to .ts; fix the two
agent-doc references so lint:doc-links resolves.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* Fix OP withdrawal claim after connecting a wallet on L2.

useWalletClient was pinned to the parent chain and stayed undefined after switchChain; send the claim through useWriteContract so the client is resolved at mutation time.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Surface batch-transaction recipients in the tx details To & Value fields (blockscout#3664)

* Add spec for tx details "to" & "value" UI/UX changes (blockscout#3661)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Restyle token-transfer "View all" link on tx details

Remove the leading navigation/tokens icon before the "View all" link in
the token-transfer detail rows and apply the secondary link style. The
overflow show-condition and Token-transfers-tab target are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Surface batch recipients in tx details "To" and "Value"

For an Eden sponsored (batch) transaction with more than one unique
recipient, the "To" field now lists the distinct recipients (first row
rich, rows 2+ bare address, capped at 5 with a "View all (N)" link) and
the "Value" field appends a "to N recipients" link. Recipients are
de-duplicated by address; both links expand the Details section.

Extract the "To" field into TxDetailsTo, derive the breakdown in a
shared getBatchRecipients helper, and add an endContent slot to
DetailedInfoNativeCoinValue for the "Value" link.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Prune task working files for tx details to/value task

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Fix batch-recipient counting and rendering in tx details "To"

Exclude contract-creation calls (null `to`) from the unique-recipient
count so "N recipients" / "View all (N)" reflect addresses only (FR-6),
and render the recipient list by matching the top-level `to` by hash
instead of assuming it is the first call — keeping the shown rows a
subset of the counted recipients in every case.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…#3677)

* Restructure the code-complexity tool into pipeline stages

The tool was 27 flat entries in one directory with four doc files scattered
among them, and CONTEXT.md served four different readers in one linear scroll.

Group the sources into a directory per pipeline stage: select/ (scope, diff),
measure/ (complexity, jsx, crap), coverage/ (read, generate), render/ (report,
github). index.ts, config.ts and analyze.ts stay at the root. Rename
coverage.ts and generate-coverage.ts to coverage/read.ts and coverage/generate.ts
so the paths stop stuttering. Specs move with their sources; no logic changes.

Split the docs by reader question and collect them under docs/: MODEL.md
(renamed from SCORING.md, now also carrying the scope rules and the jsx/behavior
classification), RUNNING.md (new: the two selection axes, generation scope, the
report, CI wiring) and CALIBRATION.md. adr/ stays at the module root, per
.agents/rules/adr.md. CONTEXT.md drops to an entry point: a routing table, the
gate-failed triage, and a file map grouped by stage.

Reframe the tool as a code-quality gate rather than a CI gate, and say plainly
that select/, measure/ and coverage/ never see a cap: the thresholds enter once,
in analyze.ts, and everything downstream reads the verdict it stamps.

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

* add vscode tasks

* Fix doc sediment and pointers in the code-complexity tool

Resolves the four review findings on the restructure:

- drop the function-class summary from CONTEXT.md (wrong rule, and MODEL.md
  already owns classification)
- retarget MODEL.md's scope pointer to ../select/scope.ts, and drop the
  circular back-reference to MODEL.md from CONTEXT.md's scope.ts line
- drop CALIBRATION.md's references to a distribution and neighbour outcomes
  the restructure deliberately removed
- state the run.sh cwd split in RUNNING.md instead of equating the wrapper
  with tools/dev-server/fetch.sh

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…d indexer (blockscout#3679)

* Add spec for scoping interchain requests to the deployment's slice

Issue blockscout#3650: one shared interchain-indexer instance replaces the
per-frontend ones, so every request has to declare its own slice —
unknown query params are silently ignored, which turns a dropped filter
into a cross-frontend data leak that returns 200.

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

* Break blockscout#3650 into four tickets

Two prefactors that can start now — the interchain types bump with its
fixtures, and the two bridge-scope env vars — then the central request
scoping and the message-details route move as independent branches.

Q03 now also tracks making BridgeInfo.id non-optional: it is optional in
1.8.1 although always sent, and the new route needs it to build a link.

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

* Type the app against interchain-indexer-types 1.8.1

The shared indexer's contract adds the filter vocabulary later tickets
send (home_chain_id, bridge_ids, include_unindexed_chains, bridge_id on
message details) and two response fields, BridgeInfo.id and
has_unindexed_chain. The bump is additive, so nothing had to be
reconciled to make it compile.

Stubs and mocks now carry both fields, so links and requests built off
them in later tickets look like the real thing.

A message whose destination side never resolves must still render as an
ordinary row with that side empty. The list components already guard
each optional field on its own; the new specs pin that, including a row
that is not flagged and still has no destination hash — which is what
fails if a later change reaches for has_unindexed_chain as a shortcut
for "this row is complete".

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

* Take the deployment's bridge scope from env

The interchain indexer is being consolidated into one shared instance per
environment, indexing every bridge and every chain. A deployment therefore
has to state which slice of it belongs to it, and it must not boot without
one: unknown query params are ignored by the service, so an unscoped
request returns another deployment's traffic with a 200 rather than an
error.

NEXT_PUBLIC_CROSS_CHAIN_TXS_BRIDGE_IDS is required whenever cross-chain
transactions are enabled, and rejected otherwise.
NEXT_PUBLIC_CROSS_CHAIN_TXS_INCLUDE_UNINDEXED_CHAINS is the escape hatch
for rows whose counterparty chain no bridge indexes, off by default.
The names are not multichain-specific because a single-chain deployment
spanning two bridges is expected.

Nothing consumes the values yet. They are read outside the feature config's
isEnabled branch: the Playwright mockApiResponse fixture builds its route
matcher in Node from playwright/.env.pw while the component reads the envs
mocked in localStorage, and NEXT_PUBLIC_CROSS_CHAIN_TXS_ENABLED is only
ever set per test, so a resolver gated on isEnabled would resolve no bridge
ids on the matching side once the requests start carrying them.

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

* Name the real check commands in tickets 03 and 04

The criteria said `pnpm test` and `pnpm lint`, neither of which is a script
in this repo. Spell out the vitest, lint and complexity commands, and point
the Playwright criteria at the Docker runner — local runs diff on rendering.

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

* Scope every interchain request to the deployment's slice

Each interchainIndexer resource declares which scope params its endpoint
accepts, and buildUrl merges the resolved profile into the query string —
one funnel, so no call site can pass or forget the params itself. Resolvers
are keyed by service as well as param name, so a same-named filter on
another API cannot pick up interchain logic and an unresolvable name is
omitted rather than fatal.

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

* Carry the bridge in the message-details URL

The shared indexer can hold the same message_id on more than one bridge, so
an unqualified lookup is a guess. The route becomes
/bridge/[bridgeId]/cross-chain-tx/[id] and the details request sends
bridge_id, which makes the page resolve identically on a fresh load, a
reload and a pasted deep link. A row with no bridge id renders unlinked
rather than falling back to a guessed route, and a malformed message id is
re-thrown as 422 so it lands on the invalid-input page instead of the 500
copy.

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

* Take interchain-indexer-types 1.8.2

The version the service now runs scopes /interchain/chains by bridge_ids,
so the counterparty selector lists the chains the deployment's own bridges
index instead of every chain on the shared instance. The resource already
declared the param, so the bump is the whole change; BridgeInfo.id and
has_unindexed_chain becoming required reconciles with no edit, since
ts-proto still renders the parent message fields as optional.

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

* fix pw tests

* Prune the ticket breakdown for blockscout#3650

The decomposition is preserved in git history, one commit per ticket, so
only the spec survives in the tree.

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

* Resolve review findings on the scope-filter work

Cover FR4 with invariants rather than a copy of the registry: every
interchain resource must declare a scope (bar message details, which
deliberately takes none), and every declared name must have a resolver.
Pinning the exact filters per resource would have to be edited in
lockstep with every legitimate change, so it would only ever catch a
deliberate edit — code review's job. Reword FR4 to that intent and
record why the stricter reading was dropped.

Reuse the cluster-chain mock in the scope-filter spec instead of an
unexplained double-cast; it has to name its indexer host, because
getResourceParams prefers a chain's own app_config.apis over the
deployment's.

Gate INCLUDE_UNINDEXED_CHAINS on CROSS_CHAIN_TXS_ENABLED, matching its
two siblings in the same schema, and resolve table cells by header text
so the spec survives a column reorder.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Add spec for diff-scoped mutation testing

Spec for issue blockscout#3665. Measures assertion strength of the existing vitest
specs: only logic mutators, only files with a co-located spec, diff-scoped
to changed lines on PRs, failing when a mutant survives.

Drops the review-changes "mutation-thinking" deliverable and the issue's
snapshot rationale from scope.

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

* Break mutation-testing spec into tickets

Seven vertical slices for blockscout#3665, plus progress.md.

Rewrites FR12: the run is bounded by wall-clock time rather than a mutant
cap. A count-based bound would have to track Stryker's own generator to stay
honest, and diff size does not predict cost — runtime is governed by
import-graph centrality. Results stream to disk as each mutant is tested, so
a truncated run still reports what completed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Make StrykerJS boot on this repo

Ticket 01 of the diff-scoped mutation testing task. No CLI yet — this is
Stryker itself working here, which every later ticket assumes.

Two pnpm integration blockers, both solved by configuration. Stryker's
minimatch@10 needs a 5.x brace-expansion, while the repo pins the package
globally to a 1.x security override; a scoped `minimatch@10>brace-expansion`
entry lifts it along that dependency path only, following the existing
vite-plugin-dts precedent. The global 1.x pin is untouched. Stryker also
discovers plugins by globbing its own scope in node_modules, which pnpm's
isolated layout defeats, so the vitest runner is named explicitly.

stryker.config.json carries everything that does not vary per run: the
logic-only mutator restriction (conditional, equality, logical, arithmetic,
boolean literal — the other twelve classes excluded), ignoreStatic and a
raised timeout for determinism, and the json + html reporters. The sandbox
lives inside the tool folder; it and the reports are gitignored, and excluded
from vitest, tsc and cspell, because a run that does not finish leaves the
sandbox behind and it is a full copy of the repo.

ignorePatterns is an allowlist rather than a denylist, so a new top-level
directory is left out of the sandbox and fails the dry run loudly instead of
being copied silently. Verified complete by running the whole suite inside
the sandbox.

The primed-request drift tests are excluded through a dedicated vitest config
rather than through the sandbox: src/server/primedRequests/index.spec.ts globs
those files off disk to assert every page has one, so removing them turns the
drift check red and aborts the dry run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Add the mutation-testing CLI and its three selection modes

Ticket 02 of the diff-scoped mutation testing task. `pnpm test:mutation-testing`
decides which files to mutate, runs Stryker over them and prints a per-file score
table read from the JSON report.

Selection mirrors the complexity gate's flags: bare takes the full eligible set,
explicit paths focus on those files, and `--changed[=<ref>]` scopes to the diff
against a base ref through the merge-base, so the branch's own commits and
uncommitted edits count while base-branch churn does not. A file is eligible only
when a vitest spec sits beside it — an untested file would otherwise produce a run
of unkillable mutants rather than findings. When the selection comes out empty the
CLI says why in one line and exits 0 without starting Stryker at all.

The diff resolution and scope allowlist are imported from the complexity gate
rather than reimplemented. Its `hasCoLocatedSpec` moves out of that tool's index
into `select/eligibility.ts` so both tools read the naming convention from one
place instead of keeping two copies in sync.

Output is built from Stryker's JSON report: the clear-text reporter prints every
mutant with its full diff and is unusable at this scale. Timeouts count as killed
and the excluded mutator classes stay out of the denominator entirely, which is how
Stryker scores it — otherwise the twelve disabled classes would dominate every row.
The report is deleted before each run, so a run that dies reports nothing instead of
presenting the previous run's results as fresh.

The sandbox allowlist grows a `tools/**` entry, since the tool's own files are the
first thing worth mutating and the gate is expected to cover repo tooling.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Mutate only behavior code, only on changed lines

Narrow what Stryker mutates from whole files to line ranges: the complement
of every `jsx` function body, intersected under `--changed` with the lines
the diff touched. Ranges restrict mutant creation rather than filtering
findings afterwards, and a file left with no mutable line is dropped from
the selection.

Classification comes from the complexity gate's AST classifier, and a line
belongs to the innermost function containing it — so a handler inside a
component is still mutated, while a render callback inside a hook is not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Bound a mutation run by wall-clock time

Ticket 04 of the diff-scoped mutation testing task. A selection that reaches
deep into the import graph can no longer grind for hours: --budget bounds any
single run, default 15 minutes, kept in the tool's config module so a local run
and a CI run bound identically.

Stryker writes its json report only when a whole run finishes, so a stopped run
would leave nothing behind. A reporter plugin closes that gap by appending one
JSON record per line as each mutant is tested, and the CLI reads that stream
back when the run did not finish. Line-delimited because a process killed
mid-write leaves a half-written last line, and every complete line before it
must still parse. Appended synchronously, one open-write-close per record: a
buffered stream would be the obvious choice, but Stryker answers a signal with
process.exit and drops whatever has not been flushed — exactly the records that
matter. The plugin is a .mts file, so emitted as ESM, because Stryker loads a
plugin through import(); it is named by path in the config's plugin list, since
pnpm's isolated layout defeats Stryker's plugin globbing.

A complete run is untouched: it still reads Stryker's own report, and the choice
between the two sources is a pure function whose spec asserts neither path
reads the other's file.

Stopping the run kills the process group rather than the process. Stryker exits
on a signal without disposing its forked vitest workers or its sandbox, so the
run is spawned detached, SIGTERM goes to the whole group, anything surviving the
grace period is killed outright, and the sandbox is removed by hand. Detaching
also puts the run outside the terminal's foreground group, so SIGINT is
forwarded — without that, quitting the CLI would leave the run behind.

A budget expiring while Stryker tears down does not count as truncation: the
json report is the honest test of whether a run finished, and the notice is
keyed off that rather than off having sent the signal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Report survivors grouped by source line

Split the mutants off Stryker's per-mutant status field: survivors are
listed under their file, grouped by the line they sit on, while
no-coverage mutants are counted on one line, being a coverage gap the
CRAP gate already owns. The HTML report's location is printed once the
run has written it.

The selection layer now takes the repository as an argument instead of
calling process.chdir(). Stryker runs vitest in worker threads, where
chdir throws, so the spec that used it was failing the dry run of every
mutation run rather than only its own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Fail a PR when a mutant on a changed line survives

The gate runs diff-scoped in its own CI job, chained on the unit tests
because a red suite makes Stryker abort during its dry run. Survivors
become inline ::error annotations on the diff; a no-coverage mutant does
not gate, being the CRAP gate's finding.

Suppressing a mutant stays possible but has to be a reviewable claim:
an ESLint rule accepts only a next-line-scoped disable that names its
mutators and gives a reason. It lives beside the tool it enforces, where
a co-located spec both tests it and makes it mutation-eligible itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Document the mutation-testing gate

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

* Kill the gate's survivors on its own code

Turning the gate on left 18 survivors in the tool it gates. Half were
missing assertions: the flag parser never proved a value flag consumes
the following token only when it has no inline one, the selection layer
never reached full mode's empty exits, the tie-break between two
functions covering the same lines was never exercised in either
direction, and the report's ordering was asserted from input that was
already sorted.

The other half were dead code, which the suppression policy says to
delete rather than defend. The table's hand-rolled padding guarded a
case the column widths rule out; the string comparator's equality
shortcut was unreachable because both call sites compare unique keys,
so the default sort does the job and stays locale-independent; the
stream's blank-line filter dropped lines that fail to parse anyway; and
the innermost-function search compared spans that the traversal order
already settles, so a tie is now what it means — the same lines.

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

* Escape the backslash when making a path literal

Minimatch's escape character was the one glob metacharacter the escaping
left alone, so a path containing a backslash had it read as escaping the
next character instead of as itself.

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

* Fail a run that never tested a mutant

A run stopped at its wall-clock budget reports whatever it got through,
and the exit code looked only at the survivors in that partial report.
A dry run that filled the budget gets through nothing, so it produced an
empty report, no survivors, and a green check — while the truncation
notice beside it said "none of the selection was covered". The warning
annotation covers a partial run staying green on purpose; it does not
cover having tested nothing.

The verdict now asks the run how far it got as well as what it found.

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

* Put a CI run's report where it gets read

A job log opens below checkout and install output, with Stryker's own
narration on top of that. The report now lands in the job summary, which
renders on the run page; Stryker's logging is folded into a collapsed
group rather than silenced, since it is the diagnosis when Stryker fails.

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

* Keep Vitest's own job summary out of the gate's

Vitest appends a github-actions reporter to its defaults whenever
$GITHUB_ACTIONS is set, and Stryker ends one test run per mutant — so the
run page filled with hundreds of "Vitest Test Report" blocks above the
report, each killed mutant reported as a test failure. Naming a reporter
in the config Stryker drives is what keeps that reporter out.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…eed (blockscout#3681)

* Let operators pick Flashblocks or Subblocks naming for the OP Stack feed

OP Stack chains are split between the legacy "Flashblocks" name and the newer
"Subblocks" name while streaming the same wire format, and one frontend image
serves both. The name was hard-coded (`subblock` since blockscout#3643), so a chain that
still runs Flashblocks had no way to keep its label.

- NEXT_PUBLIC_FLASHBLOCKS_NAME (`flashblock` | `subblock`, default `subblock`)
  now drives the `name` field of the flashblocks feature config, which every
  user-facing string already derives from. MegaETH keeps `mini-block`.
- The feature config also carries the blocks-page tab ids: the configured name
  first, the other OP Stack name as an alias, so links written under either
  name keep resolving.
- A `blocksTab` getServerSideProps guard redirects the alias to the canonical id
  with a temporary redirect, preserving the other query params. Temporary
  because the operator can flip the name later.
- Docs, a flashblocks validator sub-schema (the name requires the socket URL),
  test preset, glossary, and vitest specs for the config and the guard.

* Address review: literal tab ids, guard rename, spec via withEnvs

- Tab ids are written out literally as FLASHBLOCKS_TAB_IDS / FlashblocksTabId
  and the feature's `tabIds` is a non-empty tuple, so the ids are greppable and
  the guard's destructuring of the canonical id is sound.
- The guard is named `blocks` after the page it sits on, like its siblings.
- The guard spec uses `withEnvs` again instead of injecting a stub config.
- Rationale comments dropped from the code; docs/ENVS.md is the single place
  that states it. The glossary row keeps the rename etymology and no longer
  mentions the default.

* Address review round 2: drop the env tuple cast, narrow the MegaETH tab id type
He owns the ERC-8056 API work and is the routing target for its questions,
but was missing from the directory.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… and a restyled PR review (blockscout#3690)

* Make the review skills' inputs explicit, and add a review-file mode

Chat mode is gone — it was never used, and inferring the mode from whether a
PR existed meant a sandbox with no network silently reviewed into the void.
`review-changes` now takes its output as a required positional and everything
else as a flag: `--round` (which retires the "prior comments mean arbitration"
heuristic), `--scope`, `--as`, `--ticket`. `resolve-review` mirrors the output
input, and both carry an `argument-hint` so the `/` picker states the form.

`md` output writes the review into the task folder instead of onto a PR: one
file, one thread per finding, replies appended as blockquotes and a `Status:`
line edited in place. `--scope uncommitted` reviews the working tree before the
ticket is committed and writes beside the ticket; `--scope branch` writes at the
task root. Both are pruned at land, like the rest of the scaffolding. Where the
agent cannot tell whose the uncommitted changes are, it asks rather than guesses.

`--as <tag>` is what lets two agents review one change at once: it prefixes the
finding ids and the review file name, and a follow-up round rules only on its
own tag's findings. Untagged stays the default, since one reviewer is the usual
case, and a tag derived from the running model would change under the model and
orphan round 1's threads. A first round that finds someone else's review already
posted stops and asks for a tag.

SKILL.md is 229 lines down to 180, holding the spine only: the axis briefs and
the finding shape move to axes.md, where the axis subagents read them directly
instead of having them re-transmitted, and the publishing mechanics split into
output-pr.md and output-md.md. Dropped along the way: the `gh pr view` exit-code
rationale that gh-commands.md already carried, and the mode-detection prose the
inputs replace.

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

* Drop bot reviewers from the review skills

Reviews are done by agents running these skills, not by PR bots, so the
bot source, its `user.type` test and the bot status-post noise all
described a path that no longer exists. The `— Reviewed by …` footer is
now the only test separating this workflow's own review from a human's.

Also delete `.cursor/BUGBOT.md`, dead config once Bugbot stopped
reviewing, and align the out-of-bounds comment bullet with the hardened
comments rule in `code-quality.md`.

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

* Restyle the PR review output

Severity emoji on every finding title, id and severity alone on the first
line. Findings with no diff anchor move out of the review body into their own
issue comment. The summary is a status line plus open counters, the same shape
in every round, with no findings table.

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

* Close out the review of the review skills

Seventeen findings from two concurrent reviewers, fixed across the skills and
their reference files.

Two loops could not close. The `--as` guard stopped a first round whenever any
tagged review existed, so the concurrent-reviewer case it was written for was
unreachable; it now stops only when the runner has no tag of its own. And the
`md` · `branch` clean-tree precondition applied to every round, while
`resolve-review` leaves its `md`-mode fixes uncommitted on purpose — the
follow-up round that exists to verify those fixes always found a dirty tree and
stopped. A `md` follow-up now has no clean-tree precondition.

Deferred nits had no ruling anywhere, so a follow-up round could never satisfy
its own done-when once a nit was open: both ruling tables gain a `deferred` row,
and the `md` status keeps such a finding out of the next Gate 1. Bots get their
`user.type` row back — CodeQL and Copilot still reach a PR, and without the row
their comments were unrejectable. `md` mode drops `needs-human` instead: only
agents read that file, so the escape hatch there is `resolve-review`'s Gate 1,
a terminal exchange with the developer.

The rest are contract mismatches between files that have to agree: the
non-anchorable split pattern now accepts a tag prefix, the follow-up ruling
comment carries the footer that marks it as a ruling, `headRefOid` joins the
discovery fields, `code-reviewer.md` gates on `cleared` and reads back a
location step 5 now records, `resolve-review` takes `--scope` and `--ticket` so
its review file is named rather than inferred, and the outcome rule and the
footer rationale each live in one place with the others pointing at it.

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

* Fix the review skills' source split and tagged ruling ids

Three findings from the second review round, all regressions from the
first round's fixes:

- `gh-commands.md` still claimed the footer was the only source test and
  that every non-footer comment was a human's, contradicting the
  restored `user.type == "Bot"` row it is cited as the rationale for.
  Scope that claim to workflow-vs-human and point at
  `resolve-review/SKILL.md`'s table for the full split.
- `deferred` was missing from the threads to leave unresolved, so an
  agent would close a deferral the round had just granted.
- Non-anchorable ruling lines were written bare `F<n>`, which matches no
  tagged finding id and would be ambiguous between two reviewers.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…3691)

* Let operators pick which color themes an explorer offers

New optional NEXT_PUBLIC_COLOR_THEMES lists the themes an instance exposes;
unlisted ones are hidden, and a single-theme list drops the switcher entirely,
so an explorer like Arc can offer light and dark only.

The list filters the canonical theme order rather than reordering it, and it
feeds default resolution: when the available themes cannot cover
prefers-color-scheme, colorTheme.default becomes the darkest available theme, so
a narrowed instance renders SSR-correct instead of flashing the wrong mode until
the client effect corrects it. NEXT_PUBLIC_COLOR_THEME_DEFAULT stays as it was —
deprecating it would force a config migration on every deployment — and the
validator now rejects a default that the list does not offer.

SettingsColorTheme keeps the theme-applying effect it always had and only skips
its own markup below two themes: the popover mounts it eagerly, so a hidden
switcher still writes the CSS vars and primes the cookies. The middleware resets
a cookie holding a theme the instance no longer offers, mirroring addressFormat.

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

* Keep a returning user's color mode when their theme is dropped

The middleware reset an unavailable COLOR_THEME cookie to the configured
default, but a theme list covering both color modes leaves that default
undefined, so the reset fell through to themes[0] — light. A returning
midnight + dark visitor was flipped to light for good: the Set-Cookie lands
before JS, so SettingsColorTheme finds light available and never re-resolves
against the color mode.

Honour the COLOR_MODE cookie where there is one, and document the implicit
default that config.ts derives when the offered themes cover a single mode.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
… supported chains (blockscout#3693)

Use the Pro API in Hardhat and Foundry verification instructions

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…sted and gated (blockscout#3694)

* add the task spec for rewriting the tools/ .mjs scripts in TypeScript

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

* break the tools/ TypeScript rewrite into four tickets

Three independent ports on the frontier — the preset-list sync tool, the React
profile aggregator, and the doc-link checker as tools/doc-links/ — plus the
doc-link scope extension behind the rewrite.

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

* Rewrite the preset-list sync tool in TypeScript

Moves tools/dev-server/sync-preset-lists.mjs to tools/dev-server/sync-presets/,
bringing it under pnpm lint:tsc and the unit suite so the complexity gate reports
real coverage instead of 0%. Behaviour is unchanged: same arguments, same output,
same exit codes.

The tool gets its own tsconfig.json rather than reusing tools/dev-server/'s —
adding an outDir there would relocate fetch.js out from under the Dockerfile's
COPY.

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

* Rewrite the React profile aggregator in TypeScript

Moves tools/profiling/aggregate-react-profile.mjs to aggregate.ts plus an index.ts
CLI entry, bringing it under pnpm lint:tsc and the unit suite so the complexity gate
reports real coverage instead of 0% — replayOperations scored CRAP 182 purely
because nothing tested it. Behaviour is unchanged: verified byte-identical output
against the old script across single-file, compare and every option combination.

Specs run on hand-built wire-format fixtures rather than a committed DevTools
export: an export would pin the fixtures to one DevTools version, and the wire
format is exactly what is under test.

The ADD record handling moves into its own function. A faithful port left
replayOperations at cognitive complexity 20 against a cap of 20, so any later edit
to it would have broken the gate.

Carries the round-1 review resolution: the .mjs's usage/options header, the type
section banners and the fixture slot-layout comments are gone, the CONTEXT.md file
table is back to the importable-vs-CLI split, and the two CLI defaults are named
constants.

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

* Rewrite the doc-link checker as tools/doc-links/

Split the checker into modules by concern — surface collection, line
stripping, reference resolution, per-line checking, reporting — with the
CLI in its own entry file. Decomposing the per-file check brings its
cognitive score from 58 to 13, under the cap of 20.

The repo root now comes from git rather than from hops counted off the
entry file, which runs one directory deeper once compiled.

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

* Check every markdown file beside a CONTEXT.md, not just that file

A directory documenting itself across several files got one of them
checked, so the relative references inside the others could break
silently. The collection step now takes every tracked markdown file in
and below a CONTEXT.md directory, nested subdirectories included, which
brings the two ADRs and three docs under tools/code-complexity/ and the
three under tools/mutation-testing/ into the surface — 68 files to 76.

Two of their references did not resolve, both writing ./run.sh from a
docs/ subdirectory where the file sits one level up. Fixed as docs: the
resolver still resolves ./ against the file's own directory, because
that is what ./ means everywhere else in the instruction surface.

src/toolkit/package/ is exempt by path in the existing exclusion list —
its readme addresses npm consumers rather than agents. A README.md
filename rule would instead unprotect every module readme that
.agents/rules/docs.md expects checked.

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

* Prune the working files of the tools/ TypeScript rewrite task

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

* Name the DevTools op codes and drop the restating comments

Three review nits on the tools/ rewrite:

- `check.ts` and `resolve.ts` opened with headers that restated their row in
  `tools/doc-links/CONTEXT.md` word for word, and `index.ts` restated it before
  getting to why the checker exists. The role has one home now.
- The `strip.ts` row named `ILLUSTRATION_FORMS`, which the constraint below the
  map already owns.
- `replayOperations` switched on the raw op codes 1-7 with comments that said
  what a name would. The operand layouts stay — they are what makes the index
  arithmetic legible.

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

* Kill the surviving mutants in the rewritten tools/ files

Two of them marked redundant code rather than a missing assertion: the
`endIndex === -1` operand in the marker guard is unreachable behind the
ordering test, and comparing a path to an excluded entry adds nothing to
the prefix test beside it. The rest were assertions the specs never made —
link forms without an anchor or a slash, the boundary of the string-table
range, a zero delta, and skip-width fixtures whose operands doubled as op
codes, so a mis-skipped record resynchronised on its own operands.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
)

* Let operators set the label on the DeFi dropdown button

The trigger's text was hardcoded to "Blockscout DeFi", so an instance
could configure the dropdown's actions but not what the button calling
them is named.

Resolves blockscout#3687

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

* Address review on the DeFi button text env

Gate the button-text env on the items env with `.when(...)`, the companion
pattern the validator's CONTEXT.md prescribes, and annotate the spec helper's
return type. Note why the config spec primes `src/config` first: without it the
import graph is entered mid-cycle and `app.baseUrl` reads undefined.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Enforce the app-config convention with ESLint rules

Three rules, all errors: a config module has no export besides its default;
a config module is imported only from another config.ts, src/config/** or
its own config.spec.ts; src/config/utils/envs.ts is imported only from a
config.ts or src/config/**. Scope is every module the root aggregator
assembles. The convention and its exemptions are documented in
src/config/CONTEXT.md.

eslint-plugin-boundaries had no import resolver, so the pre-existing
src-api boundary rule never fired; wiring eslint-import-resolver-node
revived it and surfaced two pre-existing violations, marked with
explanatory eslint-disable comments.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hv8kJu53gsF1oCGfWhPT1i

* Resolve review findings on the app-config lint rules

- keep the api layer policy on src/api/config.ts by selecting the rule's
  source by path rather than by element type (F1)
- document the real destination for values moved out of a config module,
  and match the lint message (F2)
- point at env-vars.md for the config sections; shorten the import
  message; describe the config.spec.ts exemption by file name (F3, F4)
- explain the fixture disable in scope-filters.spec.ts (F5)
- restrict direct window.__envs reads outside getEnvValue (F6)
- write the companion globs as placeholders so doc-links resolves them

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hv8kJu53gsF1oCGfWhPT1i

* [skip ci] Keep the window.__envs restriction on where process.env is allowed

The override for pages, server, playwright and tooling files turned the
whole no-restricted-properties rule off. Relax only the process.env entry
there; the raw browser env map stays readable by getEnvValue alone (F7).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hv8kJu53gsF1oCGfWhPT1i

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…tion pages (blockscout#3738)

* Make the app action button a header element on token, NFT and tx pages, driven by metadata

Drop NEXT_PUBLIC_TOKEN_ACTION_BUTTON_CONFIG (never released) and the
token-action-button feature. The app action button from the metadata
service's protocol tag now renders in the page header of the token,
NFT instance and transaction pages, with {address}/{chainId}/{txHash}
placeholders and utm params on the link. Tags whose meta carries
hidden: "true" are read for the button but never rendered as tags.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix position of the button on NFT item page

* [skip ci] temporary disable env validation for review env

* Resolve review findings on the app action button

- fill {chainId} with the viewed chain's id on multichain pages
- pick the first protocol tag that carries an app action
- skip hidden protocol tags in tx lists, hot contracts and tx details
- type the mixpanel Action button source as AppActionSource
- share the metadata response mock helper across Playwright tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Cover the marketplace app-id branch in getAppActionData spec

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* merge tags and button in one container and move button to the title first row on tx page

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
… first visit (blockscout#3741)

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* Mock react-google-recaptcha in Playwright component tests

Google's reCAPTCHA script occasionally slips past the global request
interceptor, renders with the fake site key and fires onErrored, so the
initialization warning randomly appears in TagSubmitionForm screenshots.

Part of blockscout#3740

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Wait for Multisend chains info request before taking screenshot

The widget requests chains info only after the lazy wallet stack mounts,
so under full-suite load the token input's loader could still be visible
when the screenshot stabilized.

Part of blockscout#3740

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Re-hover AddressEntity before the hover screenshot

In the test's small viewport the tooltip's first, not yet positioned frame
covers the cursor. When the highlight delay expires during that frame, the
entity is not hovered and the highlight is skipped for good, since React
does not re-fire mouseenter when the pointer returns from the portaled
tooltip. Re-entering the entity after the tooltip is positioned makes the
highlight land every time.

Part of blockscout#3740

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Wrap mounted component in TestApp so the mount locator always targets #root

Playwright's mount locator resolves to the only child of #root, or to #root
itself when there are several. When a component's first render is null
(e.g. useIsMounted in TokenInstanceDetails), the color mode script is the
only child, the locator sticks to it and the screenshot times out waiting
for it to become visible. A display: contents wrapper keeps two children in
#root without changing layout.

Fixes TokenInstanceDetails base view and Address3rdPartyWidgets mobile base view.
Part of blockscout#3740

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Use auto-waiting row assertions in address socket tests

locator.count() does not wait, so the socket tests could count the loading
skeleton rows (21 instead of 2) when the list response had not rendered yet.
toHaveCount() and toHaveText() retry until the expected state appears.

Part of blockscout#3740

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Fix pending transactions not updating from socket on address txs tab

The socket handler replaced the updated transaction inside the cached list
in place. React Query's structural sharing then found the new data deep-equal
to the already mutated old data and kept the old reference, so the list never
re-rendered and a pending transaction stayed pending. The handler now updates
a copy of the list, and the socket test asserts the row actually updates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Re-hover EnsEntity protocol icon before the tooltip screenshot

The first hover mounts the lazy tooltip, which remounts the icon and briefly
swaps it with the image loading skeleton. With a still cursor the browser
may leave the hover state on the removed skeleton, so the lazy open check
fails and the tooltip never opens. Re-entering the settled icon opens the
tooltip through a regular pointer event.

Part of blockscout#3740

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Wait for the aborted chain logo in the ZetaChain icon stub test

The stub replaces the image loading skeleton only after the global request
interceptor aborts the unmocked chain logo request. Under load the abort
could land after the screenshot stabilized on the skeleton.

Part of blockscout#3740

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* update screenshot

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…lockscout#3736)

* Switch default theme selection, navigation and top bar colors to neutral

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Align dark selected control and option colors with the design, update screenshots

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix screenshots

* rollback colors for selected.option.bg

* [skip ci] change styles for "select" tag

* update screenshots

* yet another colors update

* update screenshots

* fix screenshot

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…blockscout#3744)

* Support the xDai bridge and native tokens in cross-chain transactions

The interchain indexer v1.11.0 reports a token kind on cross-chain
token objects and serves a null address for a native token. Map that
kind onto the core token model, render native tokens without a token
link or address line, and tag every bridged token row with its type.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Drop a fixture comment that restated the mock

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Rebaseline cross-chain token transfers screenshots after merging main

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix screenshot

* Park the cursor before the flattened-source verification screenshot

After the "Add item" click the cursor stayed over whichever "Library address"
input the scroll offset put under it, so the dark-mode baseline carried a hover
border that CI rendered on the other input. Move the mouse to the empty right
margin and rebaseline.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Add spec for PostHog analytics integration (blockscout#3754)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Break PostHog analytics spec into tickets (blockscout#3754)

Three agent-only vertical slices: extract the provider-neutral analytics
facade, generalise the buffer queue to N providers, add the PostHog provider.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Extract provider-neutral analytics facade from the Mixpanel module

Move the app's analytics API (logEvent, EventTypes, userProfile, reset,
useInit, useLogPageView, getPageType) to src/shared/analytics and point
every call site at it, so the Mixpanel module is reached only through the
facade. Drop the unused userProfile.increment, rename the debug switch to
_analytics_debug, and add a useRevoke spec for the touched revoke flow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Generalise the analytics buffer queue to multiple providers

Move the buffer queue from the Mixpanel module into the analytics facade and
fan it out to any number of providers behind one AnalyticsProvider interface.
Each enabled provider's SDK is initialised once, set up (super-props,
identity, profile writes), then the buffer is replayed into it with the
original timestamp; a provider whose init or setup throws is dropped on its
own. The Mixpanel module shrinks to a thin provider, the enabled-provider
list lives in the facade's provider registry, and an ESLint rule keeps call
sites off vendor modules. The send_immediately option becomes the
provider-neutral sendImmediately track option.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Add PostHog as a second analytics provider

PostHog is enabled by NEXT_PUBLIC_POSTHOG_API_KEY (with optional
NEXT_PUBLIC_POSTHOG_CONFIG_OVERRIDES), gated on private mode and the
Usercentrics "PostHog" consent, and receives the same events, identity
and person properties as Mixpanel through the shared analytics queue.
Page views map to the native $pageview event; replayed events keep
their original timestamp. The CSP admits *.posthog.com only when the
provider is enabled. posthog-js is pinned and loaded as a lazy chunk.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Prune PostHog analytics task working files

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Address review nits on PostHog analytics PR

Mark NEXT_PUBLIC_POSTHOG_API_KEY as Required, rename leftover
Mixpanel-named analytics bindings, and declare useInit's return type.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Test the analytics queue against real provider registry; restore PickByType

Drop the ./providers mock from the queue spec and enable providers via
withEnvs instead. Restore the PickByType utility type removed with its
last call site.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Exclude posthog-js from the license check

Its dual MIT/Apache-2.0 license expression is not recognised by
license-report-check, same as @helia/verified-fetch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Close mutation-testing gaps in PostHog provider and analytics queue

Cover sending a live event immediately without a timestamp, and suppress
the equivalent mutant on the queue's track-only timestamp narrowing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…scout#3765)

* Fix code editor crash when contract sources repeat a file path

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Address review: declare return types and strengthen editor-data tests

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…eme (blockscout#3763)

Fix the white flash on reload in Firefox for dark color themes

Render the color mode script in <head> so the theme class is set on <html>
before the first paint, instead of only after the global styles in <body>.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…coin (blockscout#3762)

Fix missing decimals in the withdrawals counter sum

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…blockscout#3769)

Fix search on the bridged tokens tab with cross-chain txs enabled

The interchain-indexer bridged_tokens resource did not declare q in its
filterFields, so since the paginated list moved to URL-derived filters
the search term was pushed to the URL but never sent to the API.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…t#3780)

* VerifyWith: mount all instance of popover immediately

* change text in VerifyWith popover

Resolves 3rd party explorers - change wording in popup
Fixes blockscout#3731

(cherry picked from commit da25e8a)
…ds after first paint (blockscout#3788)

* Keep a suspending tab panel from hiding the whole tabs tree

In Dynamic wallet-connector mode the lazy wallet hook suspends the first
wallet consumer mounted after first paint into the root Suspense boundary.
Hiding the app disconnected the tabs' presence machines, so the previously
selected panel never hid (Code content stayed visible under Read/Write proxy).
Each tab panel now has its own Suspense boundary.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Register the dynamic wallet hooks eagerly instead of suspending after first paint

In dynamic connector mode the wallet/account hooks resolved their @dynamic-labs
implementation through a dynamic import plus React.use(), so the first consumer
mounted after first paint suspended the root Suspense boundary. React hides the
app and disconnects layout effects while that resolves, which stops every mounted
zag presence machine; a tab switched in that window never hid its previous panel.

DynamicProvider already ships the whole @dynamic-labs stack in its own chunk, so
the laziness bought nothing in dynamic mode. It now registers the real hooks at
module scope through a small registry the synchronous hubs read; nothing suspends
after first paint and the root Suspense boundary goes. The reown/fallback bundle
is untouched: the hubs still never import the dynamic hooks statically.

Reverts the per-panel Suspense guard in TabsContent and its spec, which only
masked this trigger.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Tighten the dynamic hooks registry types

Declare the return type of registerDynamicHooks, mark the registry fields readonly, and keep the interface module-private since nothing imports it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 20d7581)
The PostHog env vars shipped in v2.12.0, which left them marked "upcoming".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Code review skipped — your organization has no extra usage available to pay for this review.

If your organization's extra usage balance is empty, an organization admin can add extra usage credits at claude.ai/admin-settings/usage. If its monthly spend limit was reached, an admin can raise it on the same page. If neither applies, contact Anthropic support.

Once extra usage is available, someone with write access to this repository can comment @claude review on this pull request to trigger a review.

@coderabbitai

coderabbitai Bot commented Oct 20, 2026

Copy link
Copy Markdown

Walkthrough

Warning

Review details and warnings were omitted to fit the comment limit.


afterEach(() => {
cleanup();
document.cookie = 'rewards_api_token=; Max-Age=0; path=/';
afterEach(() => {
cleanup();
queryClient.clear();
document.cookie = 'rewards_api_token=; Max-Age=0; path=/';

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces significant infrastructure and tooling improvements, including a new PostHog analytics provider, a migration of paginated lists to table-based layouts, and the addition of mutation testing and cognitive complexity gates. My review identified several potential runtime errors related to optional chaining in analytics configuration access and a few TypeScript narrowing issues in the new filter handling logic. I have provided actionable suggestions to address these stability concerns.

Status: 'Started',
Type: initialScreen.type === 'connect_wallet' ? 'Wallet' : 'Email',
Source: mixpanelConfig?.account_link_info.source ?? 'Profile dropdown',
Source: analyticsConfig?.account_link_info.source ?? 'Profile dropdown',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Since analyticsConfig is optional, accessing analyticsConfig?.account_link_info.source will throw a TypeError if analyticsConfig is undefined. Use optional chaining on account_link_info as well to prevent runtime crashes.

Suggested change
Source: analyticsConfig?.account_link_info.source ?? 'Profile dropdown',
Source: analyticsConfig?.account_link_info?.source ?? 'Profile dropdown',

Status: 'Finished',
Type: screen.type === 'success_wallet' ? 'Wallet' : 'Email',
Source: mixpanelConfig?.account_link_info.source ?? 'Profile dropdown',
Source: analyticsConfig?.account_link_info.source ?? 'Profile dropdown',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Since analyticsConfig is optional, accessing analyticsConfig?.account_link_info.source will throw a TypeError if analyticsConfig is undefined. Use optional chaining on account_link_info as well to prevent runtime crashes.

Suggested change
Source: analyticsConfig?.account_link_info.source ?? 'Profile dropdown',
Source: analyticsConfig?.account_link_info?.source ?? 'Profile dropdown',


onNextStep(screen);
}, [ initialScreen, mixpanelConfig?.account_link_info.source, onNextStep, csrfQuery, queryClient, onRewardsLoginSuccess ]);
}, [ initialScreen, analyticsConfig?.account_link_info.source, onNextStep, csrfQuery, queryClient, onRewardsLoginSuccess ]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Since analyticsConfig is optional, accessing analyticsConfig?.account_link_info.source will throw a TypeError if analyticsConfig is undefined. Use optional chaining on account_link_info as well to prevent runtime crashes.

Suggested change
}, [ initialScreen, analyticsConfig?.account_link_info.source, onNextStep, csrfQuery, queryClient, onRewardsLoginSuccess ]);
}, [ initialScreen, analyticsConfig?.account_link_info?.source, onNextStep, csrfQuery, queryClient, onRewardsLoginSuccess ]);

Comment on lines +61 to +62
analyticsConfig?.account_link_info.source !== 'Profile' && analytics.logEvent(analytics.EventTypes.ACCOUNT_LINK_INFO, {
Source: analyticsConfig?.account_link_info.source ?? 'Profile dropdown',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Since analyticsConfig is optional, accessing analyticsConfig?.account_link_info.source will throw a TypeError if analyticsConfig is undefined. Use optional chaining on account_link_info as well to prevent runtime crashes.

        analyticsConfig?.account_link_info?.source !== 'Profile' && analytics.logEvent(analytics.EventTypes.ACCOUNT_LINK_INFO, {\n          Source: analyticsConfig?.account_link_info?.source ?? 'Profile dropdown',

});
}
}, [ recaptcha, sendCodeFetchFactory, isAuth, onSubmit, mixpanelConfig?.account_link_info.source ]);
}, [ recaptcha, sendCodeFetchFactory, isAuth, onSubmit, analyticsConfig?.account_link_info.source ]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Since analyticsConfig is optional, accessing analyticsConfig?.account_link_info.source will throw a TypeError if analyticsConfig is undefined. Use optional chaining on account_link_info as well to prevent runtime crashes.

Suggested change
}, [ recaptcha, sendCodeFetchFactory, isAuth, onSubmit, analyticsConfig?.account_link_info.source ]);
}, [ recaptcha, sendCodeFetchFactory, isAuth, onSubmit, analyticsConfig?.account_link_info?.source ]);

Comment on lines +25 to +29
if (hasFocalChain || crossChainTxsFeature.bridgeIds.length === 0) {
return;
}

return crossChainTxsFeature.bridgeIds.join(',');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If crossChainTxsFeature.bridgeIds is undefined or not loaded yet, accessing .length will throw a TypeError. Use optional chaining or fallback to an empty array to prevent runtime crashes.

    const bridgeIds = crossChainTxsFeature.bridgeIds;\n\n    if (hasFocalChain || !bridgeIds || bridgeIds.length === 0) {\n      return;\n    }\n\n    return bridgeIds.join(',');

Comment on lines +125 to 135
const handleFilterChange = React.useCallback(<T extends keyof AdvancedFilterParams>(field: T, val: AdvancedFilterParams[T]) => {
if (pendingFilters.current === null) {
pendingFilters.current = { ...latestFilters.current };
queueMicrotask(() => {
const nextFilters = pendingFilters.current ?? {};
pendingFilters.current = null;
onFilterChange(nextFilters.age ? omit(nextFilters, AGE_RANGE_FIELDS) : nextFilters);
});
}
pendingFilters.current[field] = val;
}, [ onFilterChange ]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

TypeScript control flow analysis might not narrow pendingFilters.current to be non-null after the if block, leading to 'Object is possibly null' compilation errors under strict null checks. Using a local variable safely narrows the type.

  const handleFilterChange = React.useCallback(<T extends keyof AdvancedFilterParams>(field: T, val: AdvancedFilterParams[T]) => {\n    let currentPending = pendingFilters.current;\n    if (currentPending === null) {\n      currentPending = { ...latestFilters.current };\n      pendingFilters.current = currentPending;\n      queueMicrotask(() => {\n        const nextFilters = pendingFilters.current ?? {};\n        pendingFilters.current = null;\n        onFilterChange(nextFilters.age ? omit(nextFilters, AGE_RANGE_FIELDS) : nextFilters);\n      });\n    }\n    currentPending[field] = val;\n  }, [ onFilterChange ]);

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces major tooling and feature upgrades, including upgrading Node.js to v24, integrating PostHog analytics via a provider-neutral facade, adding cognitive-complexity and mutation-testing gates, and optimizing Playwright visual tests. It also implements ERC-8056 token multiplier support, replaces mobile list views with scrollable tables, and refactors paginated query hooks to prevent redundant requests. Feedback on the changes highlights a potential runtime error in buildAppActionUrl when parsing relative URLs, and a potential undefined assignment in AddressFilter when handling empty arrays.

.replaceAll('{txHash}', placeholders.txHash ?? '');

try {
const url = new URL(substituted);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The new URL() constructor throws if the URL is relative. If template can be a relative path, this will cause a runtime error. Use a base URL to safely resolve relative paths.

try {
    const url = new URL(substituted, 'https://blockscout.com');
    url.searchParams.set('utm_source', 'blockscout');
    url.searchParams.set('utm_medium', UTM_MEDIUM[source]);
    return url.toString();
  } catch {
    return undefined;
  }

Comment on lines +108 to +110
const newVal = [ ...prev ];
newVal[index] = { ...newVal[index], mode: value[0] as AddressFilterMode };
return newVal;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Accessing value[0] without checking if the array is empty can lead to undefined being assigned to mode, which may cause runtime issues if AddressFilterMode expects a string.

const newVal = [ ...prev ];
      if (value.length > 0) {
        newVal[index] = { ...newVal[index], mode: value[0] as AddressFilterMode };
      }
      return newVal;

@JOY
JOY (JOY) force-pushed the sync-upstream-v2.12.1 branch 4 times, most recently from 82f82ab to d3dd61d Compare October 21, 2026 14:02

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Code review skipped — your organization has no extra usage available to pay for this review.

If your organization's extra usage balance is empty, an organization admin can add extra usage credits at claude.ai/admin-settings/usage. If its monthly spend limit was reached, an admin can raise it on the same page. If neither applies, contact Anthropic support.

Once extra usage is available, someone with write access to this repository can comment @claude review on this pull request to trigger a review.

@JOY
JOY (JOY) force-pushed the sync-upstream-v2.12.1 branch from d3dd61d to 19e9240 Compare October 21, 2026 20:02
@JOY
JOY (JOY) force-pushed the sync-upstream-v2.12.1 branch from 19e9240 to 7632c41 Compare October 22, 2026 02:04
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.

8 participants