Skip to content

Sync upstream v2.12.0 (merge conflicts) - #32

Open
JOY (JOY) wants to merge 83 commits into
mainfrom
sync-upstream-v2.12.0
Open

JOY (JOY) wants to merge 83 commits into
mainfrom
sync-upstream-v2.12.0

Conversation

@JOY

Copy link
Copy Markdown

Upstream Sync - v2.12.0

Auto-merge with upstream v2.12.0 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.0 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>
…#3700)

* Spec ERC-8056 token multiplier support

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PA4HAJdnnF3MTCmfjTShxm

* Break ERC-8056 token multiplier support into tickets

Six scoped tickets, two deferred briefs (history tab and inline history,
blocked on the endpoint shape), progress.md, Q03 waived, and the
.env.extra additional-types override so dev and demo deploy enable ERC-8056.

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

* Add ERC-8056 as a fungible token type with multiplier primitives

Pin @blockscout/api-types 0.0.1-beta.9aecddc69c, which carries the ERC-8056
multiplier fields on the token model and on fungible transfer totals, and
drops total_gas_used from the stats response.

Add the one predicate that resolves a token to its UI multiplier (only when
the instance enables ERC-8056 and the response carries the field) and the
one formatter for the factor, with unit specs. Default the new token model
fields, add ERC-8056 token and transfer mocks, and a shared env preset.

Search results branch on the fungible-type predicate instead of a hardcoded
ERC-20 check, so an ERC-8056 token shows its price cell and lands in the
Tokens group. The group heading lists the enabled fungible types.

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

* Scale ERC-8056 amounts in the shared primitive; holders and token details

calculateUsdValue, AssetValue and SimpleValue accept a multiplier: the
displayed amount is raw × multiplier, fiat follows the scaled amount, and
the tooltip discloses the raw value and the factor. Callers that pass
nothing render exactly as before.

Add TokenMultiplierTag in the token slice and wire the two first surfaces:
the Holders quantity column (scaled, tagged) and the token details page —
scaled total supply and a Multiplier row under Price. The NFT marketplaces
and dapp rows now key off the fungible-type predicate instead of a
hardcoded ERC-20 check, so an ERC-8056 token no longer shows them.

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

* Scale ERC-8056 balances on the address page; tokens tab, net worth, token select

The three address surfaces that computed token quantities on their own now
go through the shared calculateUsdValue with the token's UI multiplier: the
fungible tokens table and list (scaled quantity with the multiplier tag, USD
from the scaled amount), the net-worth aggregation in the address token
utils (renamed to addUsdValue), and the token-select dropdown row.

The fungible tab is titled "Fungible tokens" instead of joining the enabled
type names. Add an ERC-8056 balance mock and Playwright cases for the tokens
tab and the token select, with a unit spec for the net-worth helper.

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

* update tokens and nfts tabs labels

* Resolve Q01: API always populates the historical ui_multiplier

Verified on eth-sepolia; null stays a data gap that the predicate degrades
to unscaled rendering. Tickets 03, 04, 06 now state that behaviour directly.

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

* Scale ERC-8056 amounts on token transfer surfaces by the transfer's own multiplier

Token transfer rows (shared table/list, token tab, index page) and the
tx-details snippet now pass total.ui_multiplier through the token slice
predicate, so the amount is scaled, tagged and disclosed with the factor
that applied when the transfer happened. A null factor degrades to plain
ERC-20 rendering with no fallback to the token's current multiplier.

TokenTransferSnippet routes every fungible type to the fiat snippet
instead of matching ERC-20 only.

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

* Add a Multiplier column to the advanced filter and scale ERC-8056 amounts

The advanced filter table gains a Multiplier column between To and Amount,
showing the factor that applied to each ERC-8056 transfer row. The column
is listed and checked by default only when ERC-8056 is enabled, read from
the focused chain's config in multichain mode. The Amount cell scales those
rows by total.ui_multiplier through the transfer predicate, which now also
accepts advanced filter items.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GmCfLhjVc7hFsuEUheVn3L

* Scale ERC-8056 amounts in transaction state changes by the state change's own multiplier

The State tab renders token before / after / change through AssetValue,
scaled by the state change's ui_multiplier (the factor in force at that
block) via the token slice predicate, with the multiplier tag and the raw
value disclosed in the tooltip. A null factor degrades to plain rendering
with no fallback to the token's current multiplier. Full precision is kept
in the cell so non-ERC-8056 rows show the same numbers as before.

Bump @blockscout/api-types to 0.0.1-beta.082a03750a, which declares
StateChange.ui_multiplier, and adapt to the rest of its changes: token
counters gain ui_multiplier_changes_count, and OP dispute games gain
l2_timestamp with a nullable l2_block_number (rendered as N/A).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GmCfLhjVc7hFsuEUheVn3L

* Scope multiplier history tickets 07 and 08 against the published API types

Resolve Q02: the ui-multiplier-changes endpoint and the counter are typed in
the already pinned @blockscout/api-types beta, so no stopgap local type or
follow-up migration ticket is needed. Replace the 07 and 08 briefs with ticket
specs, and add Q07 for how a scheduled change is shown.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GmCfLhjVc7hFsuEUheVn3L

* fix skeleton loading state when text leaked due to color prop override

* Add a Multiplier history tab to the ERC-8056 token page

The tab lists every recorded UIMultiplierUpdated event with its transaction,
timestamp, block, old to new factor, activation date and derived status. It
shows on a token of type ERC-8056 whenever the instance enables that type, and
carries the ui_multiplier_changes_count from the token counters.

The multiplier components move under components/ui-multiplier/ alongside the
existing tag.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GmCfLhjVc7hFsuEUheVn3L

* Show a Scheduled status for future ERC-8056 multiplier changes

A change whose activation date is still ahead now reads Scheduled instead of
Inactive, styled like Inactive as the designer approved (Q07). Active is still
only resolved on the first page. The history table screenshot now covers all
three statuses, and it runs on desktop only.

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

* Show recent multiplier changes inline on the ERC-8056 token details

The Multiplier row gets a View history toggle when the token counters
report
at least one change. Expanding it fetches the history once and shows up
to
five of the most recent changes, with a View all link to the Multiplier
history tab when there are more. On narrow screens the table scrolls
horizontally.

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

* Prune ERC-8056 token multiplier task working files

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

* fix pw tests

* Resolve review round 1 for the ERC-8056 token multiplier

Pass the focused chain's app_config into every multiplier resolution so
cluster routes agree with the history-tab gate; fix the ERC-20 branch on
the cluster search item; show error and empty copy in the inline history;
move ChainConfig to token/types/client.

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

* bump API types package

* fix hydration error

* fix pw tests

* Support ERC-8056 token multiplier in multichain address portfolio

Bump @blockscout/multichain-aggregator-types to 2.1.8 for the new
ui_multiplier fields, apply the multiplier to quantity and USD value in
the portfolio tokens table and net-worth calculation, and render the
multiplier tag as on the single-chain address tokens page.

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

* update screenshots

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…review (blockscout#3726)

* Add the spec for the Jev review-screen pilot (blockscout#3720)

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

* Break the Jev review-screen pilot into tickets (blockscout#3720)

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

* Add the review-screen rubric: ten standards rules with a vitest spec

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

* Add the review-screen CLI: standards grid, windows, sidecar

`pnpm review:screen` resolves a change (branch or uncommitted scope, or an
explicit base and spec), renders each touched file's hunks as line-id state,
scores every rubric rule against every matching file with Jev, locates each
over-threshold cell with a choice call, and prints the suspects as JSON.
Every run writes a sidecar with all cell scores, timings and usage to the
main checkout's .ai/jev/. No key or an API failure exits 0 with a status
and reason. The spec grid is a stub until the next ticket.

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

* Score each Functional Requirement against the diff in the review screen

The spec grid asks, per touched file and window, whether the changes address
each requirement of the task spec; a requirement whose best score stays below
SPEC_THRESHOLD is a suspect with no line. Both grids share MAX_SUSPECTS,
split evenly when both overflow. The task folder is excluded from the spec
grid because its own spec files state every requirement and would hide the
gaps. The sidecar records the spec cells next to the standards cells; a
missing explicit --spec is reported in the spec block, not thrown.

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

* Record suspect fates and finding origins in the review-screen sidecar

`pnpm review:screen --origins <sidecar> --findings <path|->` reads the
review's final findings table (JSON array or Markdown tables) plus the
jev axis's drop list, matches each finding to the sidecar's suspects by
file and window span or by requirement id, and rewrites the sidecar with
an `origins` block: per suspect a fate (confirmed / merged / dropped with
reason), per finding an origin (axis / jev / both). The block is replaced
on rerun. The sidecar now records each window's line span and each
standards suspect's window, which is what the match relies on once the
diff is gone.

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

* Calibrate the review-screen thresholds against past diffs

Screen seven past diffs (four merged PRs, two of them also at the commit their review was posted on, plus this branch at T04) with the new --calibration switch, which marks a sidecar as tuning data so --report can leave it out. Keep the standards default at 0.7, lower inline-empty-default to 0.6, raise the spec threshold from 0.3 to 0.7, and exempt a module's opening comment from the explanatory-comment rule. The evidence per value is in the ticket's notes.md.

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

* Dispatch the jev axis in review-changes and record finding origins

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

* Add --report to review-screen: the pilot keep-or-kill table

Reads every sidecar in the main checkout's .ai/jev/, leaves calibration
runs out, lists sidecars without an origins block as pending, and prints
per rule the suspects sent / confirmed / merged / dropped with the top
drop reason, the jev / axis / both finding counts, and the added seconds
and input tokens per review. --json prints the same data as JSON.

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

* Prune the Jev review-screen task folder down to its spec

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

* Resolve review findings on the review-screen pilot

- origins: each jev finding goes to one suspect only, nearest by located line
- sidecar: record the screen's wall-clock span; --report uses it instead of summing overlapping calls
- standards grid: rank cells and located suspects in grid order, name no suspects after a locate failure, keep a chosen line id only when it is in the window
- spec grid: tie-break in grid order; skip with a reason when the task-folder exclusion leaves no file
- hunks: StateLine as a discriminated union, no casts; shared EXEC_MAX_BUFFER in tools/cli
- rubric: drop the module-banner and test-value exemptions
- docs: usage errors exit non-zero and write no sidecar

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

* Copy .agents into the Stryker sandbox so the rubric spec can find the cited rule files

The mutation gate's dry run failed on `rubric.spec.ts`: the sandbox only holds the allowlisted paths, and `.agents/` was not among them, so `existsSync` on every cited rule file returned false.

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

* Kill the mutants the mutation gate found in the review screen

Assertions added for the branches no spec pinned down; the redundant
top-reason and split-hunk guards deleted; equivalent mutants suppressed
with their reasons.

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

---------

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>

@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 12, 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 extensive updates across the repository, including upgrading Node.js to v24.21.0, integrating PostHog analytics under a unified facade, adding ERC-8056 token multiplier support, refactoring paginated query hooks, and replacing mobile list views with horizontally scrollable tables. It also rewrites several tools in TypeScript, optimizes Playwright test selection in CI, and introduces cognitive-complexity, mutation-testing, and Jev-review-screen gates. The review feedback is highly critical as it identifies several unresolved merge conflicts in package.json, .gitignore, and docs/ENVS.md that must be resolved immediately to prevent build and dependency installation failures.

Comment thread package.json
Comment on lines +57 to +61
<<<<<<< HEAD
"@blockscout/api-types": "0.0.1-beta.d261ddc",
=======
"@blockscout/api-types": "0.0.1-beta.40c5c457b4",
>>>>>>> v2.12.0

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

critical

Unresolved merge conflict markers found in package.json. Since this is a JSON file, these markers make the file invalid JSON, which will break pnpm install and any build step immediately. Please resolve the conflict by choosing the correct version (typically the upstream version 0.0.1-beta.40c5c457b4 when syncing upstream).

    "@blockscout/api-types": "0.0.1-beta.40c5c457b4",

Comment thread .gitignore
Comment on lines +106 to +110
<<<<<<< HEAD
.codex/worktrees/
=======
/ds-bundle
>>>>>>> v2.12.0

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

critical

Unresolved merge conflict markers found in .gitignore. Please resolve the conflict by keeping both patterns.

.codex/worktrees/
/ds-bundle

Comment thread docs/ENVS.md
Comment on lines +390 to +394
<<<<<<< HEAD
| NEXT_PUBLIC_COLOR_THEMES | `Array<'light' \| 'dim' \| 'midnight' \| 'dark'>` | Color themes the app offers in the settings menu. | - | all themes | `['light', 'dark']` | upcoming |
=======
| NEXT_PUBLIC_COLOR_THEMES | `Array<'light' \| 'dim' \| 'midnight' \| 'dark'>` | Color themes the app offers in the settings menu. | - | all themes | `['light', 'dark']` | v2.11.2+ |
>>>>>>> v2.12.0

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

critical

Unresolved merge conflict markers found in docs/ENVS.md. Please resolve the conflict by keeping the upstream version v2.11.2+.

Suggested change
<<<<<<< HEAD
| NEXT_PUBLIC_COLOR_THEMES | `Array<'light' \| 'dim' \| 'midnight' \| 'dark'>` | Color themes the app offers in the settings menu. | - | all themes | `['light', 'dark']` | upcoming |
=======
| NEXT_PUBLIC_COLOR_THEMES | `Array<'light' \| 'dim' \| 'midnight' \| 'dark'>` | Color themes the app offers in the settings menu. | - | all themes | `['light', 'dark']` | v2.11.2+ |
>>>>>>> v2.12.0
| NEXT_PUBLIC_COLOR_THEMES | `Array<'light' \| 'dim' \| 'midnight' \| 'dark'>` | Color themes the app offers in the settings menu. | - | all themes | `['light', 'dark']` | v2.11.2+ |

Comment thread docs/ENVS.md
Comment on lines 936 to +940
| --- | --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS | `[{ text: string; icon?: string; dappId?: string, url?: string, isEssentialDapp?: boolean }]` | An array of dropdown items containing the button text, icon name and dappId in Dappscout or an external url | - | - | `[{'text':'Swap','icon':'swap','dappId':'swap','isEssentialDapp':true},{'text':'Payment link','icon':'payment_link','dappId':'peanut-protocol'}]` | v1.31.0+ |
<<<<<<< HEAD
| NEXT_PUBLIC_DEFI_DROPDOWN_BUTTON_TEXT | `{ desktop: string; mobile?: string }` | Text on the button that opens the dropdown. `mobile` is the shorter form shown on narrow screens. Keep `desktop` within ~24 characters and `mobile` within ~10. | - | `{'desktop':'Blockscout DeFi','mobile':'DeFi'}` | `{'desktop':'MyChain DeFi','mobile':'DeFi'}` | upcoming |
=======

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

critical

Unresolved merge conflict markers found in docs/ENVS.md. Please resolve the conflict by keeping the upstream version v2.11.2+.

Suggested change
| --- | --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS | `[{ text: string; icon?: string; dappId?: string, url?: string, isEssentialDapp?: boolean }]` | An array of dropdown items containing the button text, icon name and dappId in Dappscout or an external url | - | - | `[{'text':'Swap','icon':'swap','dappId':'swap','isEssentialDapp':true},{'text':'Payment link','icon':'payment_link','dappId':'peanut-protocol'}]` | v1.31.0+ |
<<<<<<< HEAD
| NEXT_PUBLIC_DEFI_DROPDOWN_BUTTON_TEXT | `{ desktop: string; mobile?: string }` | Text on the button that opens the dropdown. `mobile` is the shorter form shown on narrow screens. Keep `desktop` within ~24 characters and `mobile` within ~10. | - | `{'desktop':'Blockscout DeFi','mobile':'DeFi'}` | `{'desktop':'MyChain DeFi','mobile':'DeFi'}` | upcoming |
=======
| NEXT_PUBLIC_DEFI_DROPDOWN_BUTTON_TEXT | `{ desktop: string; mobile?: string }` | Text on the button that opens the dropdown. `mobile` is the shorter form shown on narrow screens. Keep `desktop` within ~24 characters and `mobile` within ~10. | - | `{'desktop':'Blockscout DeFi','mobile':'DeFi'}` | `{'desktop':'MyChain DeFi','mobile':'DeFi'}` | v2.11.2+ |

@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 several major features and tooling upgrades, including updating the Node.js and pnpm versions, integrating PostHog analytics via a provider-neutral facade, scoping interchain indexer requests, adding ERC-8056 token multiplier support, refactoring paginated query hooks, and replacing remaining mobile list views with scrollable tables. It also adds cognitive-complexity, CRAP-score, and StrykerJS mutation testing CI gates, and rewrites tooling scripts in TypeScript. The review feedback highlights a TypeScript compilation error in AdvancedFilter.tsx due to an un-narrowed null check on a ref, and suggests using loose inequality checks (!= null) in Epoch.tsx and AddressFavoriteButton.tsx to safely guard against both null and undefined values.

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.

high

Since 'pendingFilters.current' is typed as 'AdvancedFilterParams | null', TypeScript will raise a compilation error ('Object is possibly null') when attempting to assign a value to 'pendingFilters.current[field]' directly. Using a local variable to hold and narrow the reference resolves this compilation issue safely.

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

{ ...truncationProps }
/>
{ epochQuery.data.end_block_number && (
{ epochQuery.data.end_block_number !== null && (

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

Using strict inequality '!== null' does not guard against 'undefined' values. If 'end_block_number' is missing or undefined, this check will evaluate to 'true' and attempt to render '' with an undefined number. Using loose inequality '!= null' is safer as it checks for both 'null' and 'undefined'.

Suggested change
{ epochQuery.data.end_block_number !== null && (
{ epochQuery.data.end_block_number != null && (

showEmailAlert
/>
{ formData.id && (
{ formData.id !== undefined && (

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

Using strict inequality '!== undefined' does not guard against 'null' values. If 'formData.id' is 'null', the condition will evaluate to 'true' and render the delete modal with an invalid ID. Using loose inequality '!= null' is safer as it handles both 'null' and 'undefined'.

Suggested change
{ formData.id !== undefined && (
{ formData.id != null && (

@JOY
JOY (JOY) force-pushed the sync-upstream-v2.12.0 branch 5 times, most recently from 9fcf3ce to 4bcf1a6 Compare October 13, 2026 20: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.0 branch from 4bcf1a6 to 0a5c0a6 Compare October 14, 2026 02:03
@JOY
JOY (JOY) force-pushed the sync-upstream-v2.12.0 branch from 0a5c0a6 to 5c861b2 Compare October 14, 2026 08:03
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