Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 17 additions & 10 deletions .agents/TEAM.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
# Team roster for product tasks

The teams involved in product tasks, with the members an agent may need to reach. Teams have several
people — during a grilling session (`grill-the-task`) the developer picks **one contact per relevant team
for the task**; the picks are recorded in the spec header and open questions are routed to those contacts.
The member marked **default** is the fallback when the developer has no task-specific pick.
The teams involved in product tasks, the members an agent may need to reach, and their addresses in Slack.
A registry only — who gets asked what, and where, belongs to the skills that read it (`grill-the-task`,
`to-spec`). The `default` marker in a team's table flags that team's fallback member.

Slack **member IDs** are stored deliberately so routing is deterministic (no runtime name lookup). They are
workspace-scoped identifiers, not credentials — knowing one grants no access. To find yours in Slack:
Expand All @@ -30,12 +29,25 @@ Own: mockups, missing screens/states, visual decisions.

Own: API endpoints, response models, field propagation across services, backend release schedule.

### People

| Name | GitHub | Slack member ID | Focus | |
| --- | --- | --- | --- | --- |
| Victor | @vbaranov | U8L403FEG | Core API | default |
| Nikita P. | @nikitosing | U0218K3MTC5 | Core API | |
| Leonid | @lok52 | U01KDJWBCV7 | Microservices API | default |
| Evgenii | @EvgenKor | U026N2LB01E | Microservices API: Intercahin Indexer, TAC | |
| Evgenii | @EvgenKor | U026N2LB01E | Microservices API: Interchain Indexer, TAC | |

### Groups

Slack **group IDs** start with `S`, and a group is addressed by ID rather than by handle:
`<!subteam^SXXXXXXXX>`. To find one: the group's page in the workspace's user-group settings — its URL ends
with the ID.

| Team | Slack group ID |
| --- | --- |
| Core API | S064H6TD6MA |
| Microservices API | S064073HASK |

## Frontend

Expand All @@ -48,11 +60,6 @@ Own: architecture, the delegation boundary.

## Slack channels

Product questions are asked **in a channel, not a DM**, so other teams (QA in particular) see the answers.
The default is the frontend channel below; a large feature may have its own dedicated channel — recorded in
the task's spec header — and then **all** of that task's questions go there. Channel posts always mention
the addressee by member ID.

| Purpose | Channel | Channel ID |
| --- | --- | --- |
| Default for product questions | blockscout-frontend | C03MMUTQDNU |
Expand Down
4 changes: 3 additions & 1 deletion .agents/skills/grill-the-task/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,9 @@ recommended answer, decisions put to the developer while facts are looked up, an
until shared understanding is confirmed. Skip anything the research already answered.

**Start by picking the task's contacts**: for each relevant team in `.agents/TEAM.md`, ask which member
owns this task (recommending the roster's default) — these go into the spec header. Don't ask what can be
owns this task, recommending the roster's `default` — and record that default whenever the developer has no
task-specific pick. These go into the spec header, and `to-spec` routes each open question to the contact
that owns it. Don't ask what can be
inferred: when the issue's author maps to a roster member of the relevant team (match the GitHub handle in
`.agents/TEAM.md`), record them as that team's contact without asking — the PM slot in particular is
usually just the task's author. Ask about a **dedicated Slack channel** only for **large** tasks — big
Expand Down
249 changes: 249 additions & 0 deletions .agents/tasks/3593-tx-og-title-description/spec.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Turn the `og` block into a `default`/`enhanced` template layer

| | |
| --- | --- |
| Parent spec | [../../spec.md](../../spec.md) — step 1 of #3593 |
| Status | `done` |
| Size | `medium` |
| Sub-branch | — (single commit on `issue-3593`) |
| PM | Ulyana (task author) |
| Designer | — |
| Backend | — |

## Context & goal

`generate()` today treats the `og` block as static data: `opengraph.title` is just the compiled page title,
and `opengraph.description` is `TEMPLATE_MAP[pathname].og?.description` **passed through raw** — never run
through `compileValue`, so it can hold no placeholders. That makes a per-route dynamic OG description
impossible, and `RouteTemplateRecord.og` additionally requires `description` and `image` *together*.

This subtask makes `og` a first-class template layer with the same `default`/`enhanced` mechanics as the
metadata templates, with OG title and OG description resolved independently and falling back to the
metadata values when a route declares no OG template. It is **behavior-preserving**: no route's output
changes. `/tx/[hash]`'s own templates land in subtask 4.

## Functional requirements

- `og.title` and `og.description` each accept `{ 'default'?: string; enhanced?: string }` and are compiled
with `compileValue` against the same `params` object as the metadata templates.
- `og.image` becomes independent of the other two — a route may declare an image with no templates, or
templates with no image.
- The ` | Blockscout` postfix is appended to the compiled OG title in **both** the `default` and `enhanced`
cases, still gated by `config.metadata.promoteBlockscoutInTitle`.
- A route with no `og.title` gets `opengraph.title = title`; with no `og.description`, it gets
`opengraph.description = description`. An OG template that declares only an `enhanced` variant inherits
the metadata template's `default`, so a route needing just a richer bot description doesn't restate the
base text. The description fallback is written **explicitly** even though
crawlers already do it implicitly when the tag is absent — it documents the intent and makes the
resolution rule uniform with the title's.
- New param `hash_short` — `shortenString(hash, 8)`, i.e. `0xda...671a`. Set to `undefined` (not `''`) when
the route has no `hash` query param, so `compileValue`'s truthiness check behaves.
- Every existing route's `title`, `description`, and `og:image` output is byte-identical afterwards. The
explicit description fallback is the one intended exception: routes that declare no `og.description` now
emit an `og:description` tag holding the same text crawlers previously inferred from
`<meta name="description">` — the preview a social client renders is unchanged, but five entries in
`generate.spec.ts.snap` move from `undefined` to that text. `OG_ROOT_PAGE` routes are untouched (their
compiled description stays `''`, so the tag is still omitted).

## Data & API

None.

## UI inventory

No visual surface. Files:

- `src/shell/metadata/templates/index.ts` — the `RouteTemplateRecord` interface and `OG_ROOT_PAGE`.
- `src/shell/metadata/generate.ts` — resolution and the new param.
- `src/shell/metadata/generate.spec.ts` + `__snapshots__/generate.spec.ts.snap`.

## Out of scope

- Adding OG templates to `/tx/[hash]` — subtask 4.
- Touching `og:image` for any route.
- `metadata.update()` — client-side updates deliberately don't touch OG tags (bots don't run JS).

## Task breakdown

- [x] 1 `[agent]` Extend `RouteTemplateRecord` in `src/shell/metadata/templates/index.ts`
— `TemplateValue` lives in `src/shell/metadata/types.ts` and is reused by `compile-value.ts`.
- inputs:
- Extract the repeated `{ 'default': string; enhanced?: string }` shape into a named interface and
reuse it for `metadata.title`, `metadata.description`, `og.title`, `og.description`.
- New shape: `og?: { title?: <that shape>; description?: <that shape>; image?: string }`.
- Migrate `OG_ROOT_PAGE` to `{ description: { 'default': config.metadata.og.description }, image: config.metadata.og.imageUrl }`.
It is referenced by many routes; the value must stay identical.
- [x] 2 `[agent]` Resolve OG title and description independently in `src/shell/metadata/generate.ts`
- inputs:
- `const ogTemplates = TEMPLATE_MAP[route.pathname].og;`
- `opengraph.title = ogTemplates?.title ? compileValue(ogTemplates.title, params) + titlePostfix : title`
- `opengraph.description = ogTemplates?.description ? compileValue(ogTemplates.description, params) : description`
- `opengraph.imageUrl = ogTemplates?.image`
- Do **not** thread bot type into `generate()`; `apiData` presence is the only enhancement signal.
- [x] 3 `[agent]` Add the `hash_short` param in `generate.ts`
- inputs:
- Derive from `castToString(route.query?.hash)` the same way `idParam` / `idFormatted` are derived above it.
- `const hashParam = castToString(route.query?.hash); … hash_short: hashParam ? shortenString(hashParam, 8) : undefined`
- Import `shortenString` from `src/shared/texts/shorten-string`. `charNumber: 8` is what
`truncation="constant"` resolves to in the entity components, so titles and interpretation text
shorten identically.
- [x] 4 `[agent]` Cover the new layer in `src/shell/metadata/generate.spec.ts`
— new `og template layer` describe, driven by a stand-in `TEMPLATE_MAP` (no route declares OG templates yet).
- inputs:
- Test what's actually new, per `.agents/rules/tests-unit.mdc`: a route with `og` templates only
(title falls back to metadata), a route with both, `enhanced` chosen when all its params are present
and `default` when one is missing, and `hash_short` compilation.
- The existing snapshot entries must come out unchanged apart from the `opengraph.description` fallback
noted in the functional requirements — any rewrite of a `title` or `imageUrl` means the refactor
changed behavior and is wrong.
- `OG_ROOT_PAGE` routes keep emitting the same `og:description` and `og:image` as before.

## Open questions

None. (Parent Q1 does not affect this subtask.)
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Share the currency rounding and render interpretation summaries as plain text

| | |
| --- | --- |
| Parent spec | [../../spec.md](../../spec.md) — step 2 of #3593 |
| Status | `done` |
| Size | `medium` |
| Sub-branch | — (single commit on `issue-3593`) |
| PM | Ulyana (task author) |
| Designer | — |
| Backend | — |

## Context & goal

The transaction interpretation summary is a template with typed variables that only `TxInterpretation`
knows how to render — as React elements. The OG description needs the same content as a plain string.

The issue pins the amounts to the UI ("including … amount rounding"), and that rounding is four magic
thresholds living inline in `TxInterpretationElementByType`'s `currency` case. Copy-pasting them would
guarantee drift the first time someone tunes a threshold, so this subtask extracts them into a shared
function that the component then calls, and adds a plain-text renderer beside it.

## Functional requirements

- `formatCurrencyValue(value)` produces exactly what the UI shows today — the ladder from
`src/features/tx-interpretation/common/components/TxInterpretation.tsx:128`:

| range | output |
| --- | --- |
| `< 0.1` | `BigNumber(value).toPrecision(2)` |
| `< 10000` | `BigNumber(value).dp(2).toFormat()` |
| `< 1000000` | `BigNumber(value).dividedBy(1000).toFormat(2) + 'K'` |
| otherwise | `BigNumber(value).dividedBy(1000000).toFormat(2) + 'M'` |

`TxInterpretation` calls it instead of holding its own copy; its rendered output must not change.
- `summaryToPlainText(summary)` returns the summary as a single-line string, or `undefined` when
`checkSummary` rejects it — the same gate the UI uses to render nothing.
- Each variable type maps to the text its UI counterpart displays:

| type | text |
| --- | --- |
| `string` | verbatim (already inlined by `fillStringVariables`) |
| `currency` | `formatCurrencyValue(value)` |
| `token` | `symbol ?? name ?? 'Unnamed token'` — matches `TokenEntity onlySymbol` |
| `address` | `addressToPlainText(value)` (below) |
| `domain` | verbatim |
| `method` | verbatim (the badge's text) |
| `dexTag` | `value.name` |
| `link` / `external_link` | `value.name`; the URL is dropped |
| `timestamp` | `dayjs(Number(value) * SECOND).format('MMM DD YYYY')` — the UI's variable format, **not** the OG description's own timestamp format |
| `native` / `wei` | `currencyUnits.ether` / `currencyUnits.wei` |

- `getAddressName(address)` owns the name chain `AddressEntity`'s `Content` resolves — metadata `name`-type
tag (via `getTagName`) ?? `ens_domain_name` ?? `name`, `undefined` when the address has no name. It lives
in the address slice and the entity component calls it, so the two cannot drift.
- `addressToPlainText(address)` composes that with the hash fallback (`shortenString(hash, 8)`, what
`truncation="constant"` resolves to). It is exported because subtask 3's fallback action branch needs it
twice in one template string.
- Whitespace comes out clean: single spaces between parts, no leading or trailing space. Against the
production sample in the parent spec the result is exactly `Swap 2.92M SPERPS for 0.016 WETH`.

<!-- cspell:ignore SPERPS -->

## Data & API

None — operates on `TxInterpretationSummary` (`src/features/tx-interpretation/common/types/api.ts`), which
covers all ten variable types.

## UI inventory

- `src/features/tx-interpretation/common/components/TxInterpretation.tsx` — its `currency` case now
delegates. No other change; do not restructure the component to be render-agnostic.
- New files in `src/features/tx-interpretation/common/utils/` (kebab-case, matching siblings elsewhere in
the repo): the currency formatter, the plain-text renderer, the address-to-text helper.
- `src/slices/address/utils/get-address-name.ts` — the address name chain, extracted from `AddressEntity`
so both the entity and the plain-text renderer read from one place.

## Out of scope

- Noves (`createNovesSummaryObject`) — parent Q1, and it wouldn't reuse this anyway: Noves prose is already
a finished sentence.
- Making `TxInterpretationElementByType` itself render-agnostic — the one-line mappings above are cheaper
inlined in the new util than abstracted out of a working component.

## Task breakdown

- [x] 1 `[agent]` Extract the currency ladder into a shared function and call it from `TxInterpretation`
— `common/utils/format-currency-value.ts`.
- inputs:
- Signature `(value: string) => string`. Keep `BigNumber` as the implementation — same import, same
thresholds, same order of comparisons.
- The component's `currency` case becomes `<chakra.span>{ formatCurrencyValue(value) + ' ' }</chakra.span>` —
the trailing space is the component's spacing concern and stays there, out of the shared function.
- [x] 2 `[agent]` Add `addressToPlainText`
— `common/utils/address-to-plain-text.ts` over the extracted `slices/address/utils/get-address-name.ts`.
- inputs:
- Extract `AddressEntity`'s `Content` name chain into the address slice and call it from both the
component and the new helper, which appends the `shortenString(hash, 8)` fallback.
- Ignore the proxy-implementation branch (`AddressEntityContentProxy`) and the bech32/Filecoin alt-hash
handling — both are display concerns driven by client-side user settings, unavailable server-side.
- [x] 3 `[agent]` Add `summaryToPlainText`
— `common/utils/summary-to-plain-text.ts`.
- inputs:
- Return `undefined` when `!checkSummary(template, variables)`.
- Reuse the existing `fillStringVariables` → `extractVariables` → `getStringChunks` pipeline from
`../utils/utils` so the parsing stays identical to the component's.
- Assemble parts (trimmed chunk, then that index's variable text), drop empties, join with a single
space, then collapse runs of whitespace and trim. Don't try to replicate the component's per-element
trailing spaces.
- Handle `native` / `wei` by name before the type switch, exactly as the component does.
- [x] 4 `[agent]` Unit tests
— a spec per new util; the timestamp assertion pins `TZ` to UTC via `vi.stubEnv`.
- inputs:
- Reuse `txInterpretation` from `src/features/tx-interpretation/blockscout/mocks.ts` (it exercises
`string`, `currency`, `token`, `address`, `timestamp` in one template) and `TX_INTERPRETATION` from
`blockscout/stubs.ts`.
- Cover the four rounding branches at their boundaries, the name chain (name tag / ENS / name / none)
beside `getAddressName`, `checkSummary` rejection returning `undefined`, and the whitespace result.
- Skip tests that only assert the mock or `BigNumber` itself.

## Open questions

None. (Parent Q1 does not affect this subtask.)
Loading
Loading