Review changes
Review a branch or diff for the class of issue a linter structurally cannot
catch: is this the right layer for this component, does this still respect
the token contract in spirit as well as letter, and does this new code
introduce a multi-tenant performance problem that only shows up under load.
Background and the full guardrail picture live in
../../docs/15-architectural-guardrails.md
— this file is the procedure, not the rationale.
This complements CI. It does not replace it.
../../docs/15-architectural-guardrails.md
defines three enforcement layers. This skill is Layer 2 — "agentic
review." Layer 1 (dependency-cruiser, the token-contract lint, size-limit,
Lighthouse CI, tsc --noEmit, ESLint) is the hard gate: deterministic,
CI-failing, catches what a regex or a static graph can prove mechanically. If
Layer 1 is red, fix that first — this skill is not a substitute for making CI
pass, and it will not re-derive checks CI already runs deterministically and
cheaply. This skill exists for the judgment-level issues that pass every hard
gate by the letter of the rule while violating its intent: a component
correctly placed in common/ by import-count that has quietly grown
domain-specific props, a Manager/Renderer split where a useEffect crept into
the Renderer, a virtualized table that virtualizes correctly but still
fetches all 50k rows up front. Findings from this skill are advisory — output
a review comment with ranked findings, never a merge block. A human (often
the PR author, reading this output before requesting human review) decides
what to act on now versus follow up later.
When to use this
On demand against a local branch before opening a PR, or run against an
already-open PR's diff. Use it any time a change touches
src/components/**, src/modules/**/components/**, src/themes/**, a
service or hook that fetches data, or any component rendering a
tenant-scoped list. Skip it for changes with no rendering or data-fetching
surface (e.g., a docs-only change, a config file rename) — there's nothing
this skill's checks apply to.
Procedure
Get the diff.
git diff <base>...HEAD
Use the branch's actual merge-base (typically main or the PR's target
branch) as <base>. If reviewing an already-open PR, gh pr diff <number> is equivalent. Read the full diff before starting the checks
below — a finding in step 4 (e.g., an unbounded fetch) is often only
correctly diagnosed by also seeing the component in step 2 that calls it.
Check boundary and direction violations against
../../docs/09-component-architecture.md.
For every new or moved file under src/components/** or
src/modules/**/components/** in the diff:
- Confirm the file's layer (
ui/ / common/ / shared/ / modules/*)
matches what it actually knows. A component that references a domain
concept (traceId, spanStatus, tenantId) does not belong in
common/ or shared/ regardless of import count — the "does this
component know what a trace is" test from doc 09 overrides the
mechanical "does anything import it twice" test dependency-cruiser
runs.
- Confirm no new import crosses a forbidden direction (
shared/ →
modules/*, common/ → shared//modules/*, ui/ → anything to its
right, one module → another module's components/). dependency-cruiser
already fails the build on a direct component import; this check is for
the near-miss version — a shared/ component importing a modules/*
hook or util instead of a component, which is the same violation
in spirit but doesn't trip the CI graph rule.
- For any Manager/Renderer or Container/Presenter pair touched by the
diff, confirm the split still holds: no
useForm, submit handler, store
read, or side effect has crept into the Renderer/Presenter half. This is
the specific erosion doc 09 and doc 15 call out — it never crosses an
import boundary a lint rule can see.
- For any config-driven component touched by the diff, confirm the config
object holds only data, never a callback implementing validation or a
side effect (see doc 09's config-driven rendering section).
- Flag a
common/ promotion that has exactly one real call site today —
the two-call-site bar is doc 09's explicit test, and "might be reusable
later" is the named anti-pattern.
Check token-contract violations against
../../docs/08-design-tokens.md.
The token-contract CI lint already fails the build on a literal hex,
rgba(), or raw Tailwind color utility — don't re-run that regex. Check
instead for violations the regex is blind to:
- A component reaching for a
base.css primitive directly
(hsl(var(--zinc-900))) instead of the semantic name in theme.css —
passes the lint (it's a var() reference, not a literal) but breaks the
"primitives can move without components knowing" guarantee doc 08
exists for.
- A new semantic concept (a new status, a new surface, a new metric
polarity) added without a corresponding Layer 2 token in
theme.css —
look for a className/style that conditionally picks between two
existing tokens to fake a concept that deserves its own name (e.g.,
toggling between --status-warning and --status-error in JS to
represent a third state that isn't really either).
className on a ui/, common/, or shared/ component carrying
anything beyond layout (margin, padding, width, flex/grid) — visual
state passed in as a raw class instead of a variant prop breaks the
contract in doc 09 and doc 08 both describe.
- Any new light-theme branch or conditional light/dark color pick — the
app is dark-only; there should never be one.
Run the performance heuristics checklist against
../../docs/16-multitenant-performance.md.
Walk this list item by item against every new or changed component, hook,
and service method in the diff. Each item names the pattern doc 16 defines
and the failure mode it prevents.
| # |
Check |
Fails when |
Doc 16 reference |
| 1 |
Unvirtualized large lists. Does any new/changed table or list component render a data-shaped array (traces, spans, logs, alerts) without useVirtualizer or equivalent? |
The component maps the full array straight into JSX with no windowing, regardless of today's row count — "only 300 rows today" is not an exemption. |
Load-lens 1 |
| 2 |
Fetch waterfalls. Does a component's data dependencies fetch sequentially when they could fetch in parallel (a useEffect that awaits fetch A, then fetches B using nothing from A's result)? Does any view fetch the full dataset instead of a cursor/limit page? |
A service call has no cursor/limit-shaped parameter, or a hook awaits one fetch before starting an independent second one. |
Load-lens 2 |
| 3 |
Unmemoized expensive renders. Does a component re-derive a sort/filter/aggregation over a large array on every render instead of at a memoized boundary? |
A .sort(), .filter(), or .reduce() over a data-shaped array sits directly in render body, not inside useMemo keyed on the array reference. |
Load-lens 4 |
| 4 |
Cross-tenant over-fetch. Does every new service method that queries tenant-scoped data take a required tenantId (or equivalent) parameter with no "fetch all" path? |
A method can be called without a tenant scope, or an "admin" view loops across tenants and concatenates client-side instead of using an explicitly audited aggregate path. |
Load-lens 5, Rules & gotchas |
| 5 |
Missing AbortController. Does every fetch that can outlive a UI interaction (filter change, navigation, scroll-triggered page load) accept { signal } and get aborted by the caller before starting the next one? |
A hook fires a new fetch on a dependency change without aborting the prior in-flight request, or a service method has no signal parameter at all. |
Load-lens 3, Rules & gotchas |
Additionally, for any new route or route segment touched by the diff,
note which route class it falls into (shell/list, heavy data view,
settings/low-traffic — see doc 16's table) so the ranked findings in step
5 can flag if the change plausibly pushes that route past its bundle or
LCP ceiling ahead of the bundle-budget/web-vitals CI jobs actually
running.
Rank findings by severity.
Use three tiers, applied consistently:
- HIGH — will misbehave in production under realistic multi-tenant
load or will actively mislead the next person who extends this code:
missing tenant scoping (checklist item 4), an unvirtualized list on a
view that can plausibly see thousands of rows, a fetch with no
cancellation on a filter that changes rapidly (search-as-you-type,
live filters), or a boundary violation that lets
shared/ depend on a
module.
- MEDIUM — real erosion of an established pattern that won't cause an
incident today but compounds: a Manager/Renderer split with logic
leaking across it, a token-contract violation that isn't a literal but
breaks the semantic-layer guarantee, a
common/ promotion with only
one call site, an unmemoized derivation on a moderately sized list.
- LOW — style/consistency drift worth a follow-up but not worth
blocking on: a missed opportunity to reuse an existing token name, a
component that could be split for readability but isn't wrong.
Severity tracks doc 15's own posture: Layer 2 findings are advisory, so
rank by production impact, not by how easy the fix is to make.
Output an actionable list.
One line per finding: file:line (or file + component name if the diff
doesn't give a stable line number), the specific rule violated (cite the
doc), and the concrete fix — not just what's wrong. Sort HIGH → MEDIUM →
LOW. Example shape:
1. [HIGH] src/modules/traces/components/SpanTable.tsx:42 — renders
`spans.map(...)` directly with no virtualization. Fix: wrap
in `useVirtualizer` per docs/16-multitenant-performance.md
Load-lens 1 (see SpanTable worked example in that doc).
2. [HIGH] src/services/trace-service.ts:18 — `listSpans(traceId,
config)` has no `tenantId` parameter; query can cross tenant
boundaries. Fix: add required `tenantId` argument threaded
into the query, per docs/16 Load-lens 5.
3. [MEDIUM] src/modules/alerts/AlertRuleFormRenderer.tsx:67 — Renderer
calls `useEffect` to refetch severities on mount. Fix: move
the fetch into AlertRuleFormManager; Renderer stays a pure
function of `control` per docs/09-component-architecture.md
Manager/Renderer contract.
4. [LOW] src/components/common/MetricDelta.tsx:12 — reads
`hsl(var(--green-400))` directly. Fix: use the existing
`--metric-positive` semantic token per
docs/08-design-tokens.md instead of the base.css primitive.
If the diff has zero findings across steps 2–4, say so explicitly rather
than omitting the section — "no boundary, token, or performance findings
in this diff" is itself useful signal to the person reading the review.
Rules & gotchas
- This skill is advisory, not a merge gate. It never blocks a PR and
never substitutes for making the Layer 1 CI jobs in doc 15 pass — if a
literal hex or a forbidden import is in the diff, that's already a red CI
check, not a step-3 finding to rediscover.
- Don't re-run what CI already runs deterministically. Skip re-scanning
for exact hex literals, exact forbidden component-to-component imports,
or
tsc type errors — those are Layer 1's job and this skill's value is
in the checks Layer 1 structurally cannot express (see the "does this
component know what a trace is" example in step 2).
- A finding this skill repeatedly surfaces on the same shape of change is
a signal to close the Layer 1 gap, not just fix the instance. Per doc
15's own rule: if the same near-miss (e.g., a hook reaching across a
module boundary) shows up review after review, that's a
dependency-cruiser config gap, not a permanently manual check.
- Rank by production impact, not by fix difficulty. A one-line fix that
prevents a cross-tenant data leak is still HIGH; a large refactor that
only improves readability is still LOW.
- When in doubt about a boundary call, read the component's props, not
its file path. A component sitting in
common/ that accepts a span: Span prop has already leaked the domain concept in through the type
signature even if its own code never mentions "trace" — doc 09's test is
"does it know," and a prop type is knowing.
Source lineage
Net-new. Defined as Layer 2 of the enforcement model in
../../docs/15-architectural-guardrails.md;
checks are drawn from ../../docs/09-component-architecture.md,
../../docs/08-design-tokens.md, and
../../docs/16-multitenant-performance.md.
1---2name: review-changes3description: Use when reviewing a branch/diff for architectural and performance drift — checks against the arche-ui architecture docs and a performance heuristics checklist, and reports ranked findings.4---56# Review changes78Review a branch or diff for the class of issue a linter structurally cannot9catch: is this the right layer for this component, does this still respect10the token contract in spirit as well as letter, and does this new code11introduce a multi-tenant performance problem that only shows up under load.12Background and the full guardrail picture live in13[`../../docs/15-architectural-guardrails.md`](../../docs/15-architectural-guardrails.md)14— this file is the procedure, not the rationale.1516## This complements CI. It does not replace it.1718[`../../docs/15-architectural-guardrails.md`](../../docs/15-architectural-guardrails.md)19defines three enforcement layers. This skill **is** Layer 2 — "agentic20review." Layer 1 (`dependency-cruiser`, the token-contract lint, `size-limit`,21Lighthouse CI, `tsc --noEmit`, ESLint) is the hard gate: deterministic,22CI-failing, catches what a regex or a static graph can prove mechanically. If23Layer 1 is red, fix that first — this skill is not a substitute for making CI24pass, and it will not re-derive checks CI already runs deterministically and25cheaply. This skill exists for the judgment-level issues that pass every hard26gate by the letter of the rule while violating its intent: a component27correctly placed in `common/` by import-count that has quietly grown28domain-specific props, a Manager/Renderer split where a `useEffect` crept into29the Renderer, a virtualized table that virtualizes correctly but still30fetches all 50k rows up front. Findings from this skill are advisory — output31a review comment with ranked findings, never a merge block. A human (often32the PR author, reading this output before requesting human review) decides33what to act on now versus follow up later.3435## When to use this3637On demand against a local branch before opening a PR, or run against an38already-open PR's diff. Use it any time a change touches39`src/components/**`, `src/modules/**/components/**`, `src/themes/**`, a40service or hook that fetches data, or any component rendering a41tenant-scoped list. Skip it for changes with no rendering or data-fetching42surface (e.g., a docs-only change, a config file rename) — there's nothing43this skill's checks apply to.4445## Procedure46471. **Get the diff.**4849 ```bash50 git diff <base>...HEAD51 ```5253 Use the branch's actual merge-base (typically `main` or the PR's target54 branch) as `<base>`. If reviewing an already-open PR, `gh pr diff55 <number>` is equivalent. Read the full diff before starting the checks56 below — a finding in step 4 (e.g., an unbounded fetch) is often only57 correctly diagnosed by also seeing the component in step 2 that calls it.58592. **Check boundary and direction violations against60 [`../../docs/09-component-architecture.md`](../../docs/09-component-architecture.md).**6162 For every new or moved file under `src/components/**` or63 `src/modules/**/components/**` in the diff:64 - Confirm the file's layer (`ui/` / `common/` / `shared/` / `modules/*`)65 matches what it actually knows. A component that references a domain66 concept (`traceId`, `spanStatus`, `tenantId`) does not belong in67 `common/` or `shared/` regardless of import count — the "does this68 component know what a trace is" test from doc 09 overrides the69 mechanical "does anything import it twice" test `dependency-cruiser`70 runs.71 - Confirm no new import crosses a forbidden direction (`shared/` →72 `modules/*`, `common/` → `shared/`/`modules/*`, `ui/` → anything to its73 right, one module → another module's `components/`). `dependency-cruiser`74 already fails the build on a direct component import; this check is for75 the near-miss version — a `shared/` component importing a `modules/*`76 **hook** or **util** instead of a component, which is the same violation77 in spirit but doesn't trip the CI graph rule.78 - For any Manager/Renderer or Container/Presenter pair touched by the79 diff, confirm the split still holds: no `useForm`, submit handler, store80 read, or side effect has crept into the Renderer/Presenter half. This is81 the specific erosion doc 09 and doc 15 call out — it never crosses an82 import boundary a lint rule can see.83 - For any config-driven component touched by the diff, confirm the config84 object holds only data, never a callback implementing validation or a85 side effect (see doc 09's config-driven rendering section).86 - Flag a `common/` promotion that has exactly one real call site today —87 the two-call-site bar is doc 09's explicit test, and "might be reusable88 later" is the named anti-pattern.89903. **Check token-contract violations against91 [`../../docs/08-design-tokens.md`](../../docs/08-design-tokens.md).**9293 The `token-contract` CI lint already fails the build on a literal hex,94 `rgba()`, or raw Tailwind color utility — don't re-run that regex. Check95 instead for violations the regex is blind to:96 - A component reaching for a `base.css` primitive directly97 (`hsl(var(--zinc-900))`) instead of the semantic name in `theme.css` —98 passes the lint (it's a `var()` reference, not a literal) but breaks the99 "primitives can move without components knowing" guarantee doc 08100 exists for.101 - A new semantic concept (a new status, a new surface, a new metric102 polarity) added without a corresponding Layer 2 token in `theme.css` —103 look for a `className`/`style` that conditionally picks between two104 *existing* tokens to fake a concept that deserves its own name (e.g.,105 toggling between `--status-warning` and `--status-error` in JS to106 represent a third state that isn't really either).107 - `className` on a `ui/`, `common/`, or `shared/` component carrying108 anything beyond layout (margin, padding, width, flex/grid) — visual109 state passed in as a raw class instead of a variant prop breaks the110 contract in doc 09 and doc 08 both describe.111 - Any new light-theme branch or conditional light/dark color pick — the112 app is dark-only; there should never be one.1131144. **Run the performance heuristics checklist against115 [`../../docs/16-multitenant-performance.md`](../../docs/16-multitenant-performance.md).**116117 Walk this list item by item against every new or changed component, hook,118 and service method in the diff. Each item names the pattern doc 16 defines119 and the failure mode it prevents.120121 | # | Check | Fails when | Doc 16 reference |122 |---|---|---|---|123 | 1 | **Unvirtualized large lists.** Does any new/changed table or list component render a data-shaped array (traces, spans, logs, alerts) without `useVirtualizer` or equivalent? | The component maps the full array straight into JSX with no windowing, regardless of today's row count — "only 300 rows today" is not an exemption. | Load-lens 1 |124 | 2 | **Fetch waterfalls.** Does a component's data dependencies fetch sequentially when they could fetch in parallel (a `useEffect` that awaits fetch A, then fetches B using nothing from A's result)? Does any view fetch the full dataset instead of a cursor/limit page? | A service call has no `cursor`/`limit`-shaped parameter, or a hook awaits one fetch before starting an independent second one. | Load-lens 2 |125 | 3 | **Unmemoized expensive renders.** Does a component re-derive a sort/filter/aggregation over a large array on every render instead of at a memoized boundary? | A `.sort()`, `.filter()`, or `.reduce()` over a data-shaped array sits directly in render body, not inside `useMemo` keyed on the array reference. | Load-lens 4 |126 | 4 | **Cross-tenant over-fetch.** Does every new service method that queries tenant-scoped data take a required `tenantId` (or equivalent) parameter with no "fetch all" path? | A method can be called without a tenant scope, or an "admin" view loops across tenants and concatenates client-side instead of using an explicitly audited aggregate path. | Load-lens 5, Rules & gotchas |127 | 5 | **Missing `AbortController`.** Does every fetch that can outlive a UI interaction (filter change, navigation, scroll-triggered page load) accept `{ signal }` and get aborted by the caller before starting the next one? | A hook fires a new fetch on a dependency change without aborting the prior in-flight request, or a service method has no `signal` parameter at all. | Load-lens 3, Rules & gotchas |128129 Additionally, for any new route or route segment touched by the diff,130 note which route class it falls into (shell/list, heavy data view,131 settings/low-traffic — see doc 16's table) so the ranked findings in step132 5 can flag if the change plausibly pushes that route past its bundle or133 LCP ceiling ahead of the `bundle-budget`/`web-vitals` CI jobs actually134 running.1351365. **Rank findings by severity.**137138 Use three tiers, applied consistently:139 - **HIGH** — will misbehave in production under realistic multi-tenant140 load or will actively mislead the next person who extends this code:141 missing tenant scoping (checklist item 4), an unvirtualized list on a142 view that can plausibly see thousands of rows, a fetch with no143 cancellation on a filter that changes rapidly (search-as-you-type,144 live filters), or a boundary violation that lets `shared/` depend on a145 module.146 - **MEDIUM** — real erosion of an established pattern that won't cause an147 incident today but compounds: a Manager/Renderer split with logic148 leaking across it, a token-contract violation that isn't a literal but149 breaks the semantic-layer guarantee, a `common/` promotion with only150 one call site, an unmemoized derivation on a moderately sized list.151 - **LOW** — style/consistency drift worth a follow-up but not worth152 blocking on: a missed opportunity to reuse an existing token name, a153 component that could be split for readability but isn't wrong.154155 Severity tracks doc 15's own posture: Layer 2 findings are advisory, so156 rank by production impact, not by how easy the fix is to make.1571586. **Output an actionable list.**159160 One line per finding: `file:line` (or file + component name if the diff161 doesn't give a stable line number), the specific rule violated (cite the162 doc), and the concrete fix — not just what's wrong. Sort HIGH → MEDIUM →163 LOW. Example shape:164165 ```166 1. [HIGH] src/modules/traces/components/SpanTable.tsx:42 — renders167 `spans.map(...)` directly with no virtualization. Fix: wrap168 in `useVirtualizer` per docs/16-multitenant-performance.md169 Load-lens 1 (see SpanTable worked example in that doc).170 2. [HIGH] src/services/trace-service.ts:18 — `listSpans(traceId,171 config)` has no `tenantId` parameter; query can cross tenant172 boundaries. Fix: add required `tenantId` argument threaded173 into the query, per docs/16 Load-lens 5.174 3. [MEDIUM] src/modules/alerts/AlertRuleFormRenderer.tsx:67 — Renderer175 calls `useEffect` to refetch severities on mount. Fix: move176 the fetch into AlertRuleFormManager; Renderer stays a pure177 function of `control` per docs/09-component-architecture.md178 Manager/Renderer contract.179 4. [LOW] src/components/common/MetricDelta.tsx:12 — reads180 `hsl(var(--green-400))` directly. Fix: use the existing181 `--metric-positive` semantic token per182 docs/08-design-tokens.md instead of the base.css primitive.183 ```184185 If the diff has zero findings across steps 2–4, say so explicitly rather186 than omitting the section — "no boundary, token, or performance findings187 in this diff" is itself useful signal to the person reading the review.188189## Rules & gotchas190191- **This skill is advisory, not a merge gate.** It never blocks a PR and192 never substitutes for making the Layer 1 CI jobs in doc 15 pass — if a193 literal hex or a forbidden import is in the diff, that's already a red CI194 check, not a step-3 finding to rediscover.195- **Don't re-run what CI already runs deterministically.** Skip re-scanning196 for exact hex literals, exact forbidden component-to-component imports,197 or `tsc` type errors — those are Layer 1's job and this skill's value is198 in the checks Layer 1 structurally cannot express (see the "does this199 component know what a trace is" example in step 2).200- **A finding this skill repeatedly surfaces on the same shape of change is201 a signal to close the Layer 1 gap, not just fix the instance.** Per doc202 15's own rule: if the same near-miss (e.g., a hook reaching across a203 module boundary) shows up review after review, that's a204 `dependency-cruiser` config gap, not a permanently manual check.205- **Rank by production impact, not by fix difficulty.** A one-line fix that206 prevents a cross-tenant data leak is still HIGH; a large refactor that207 only improves readability is still LOW.208- **When in doubt about a boundary call, read the component's props, not209 its file path.** A component sitting in `common/` that accepts a `span:210 Span` prop has already leaked the domain concept in through the type211 signature even if its own code never mentions "trace" — doc 09's test is212 "does it know," and a prop type is knowing.213214## Source lineage215216Net-new. Defined as Layer 2 of the enforcement model in217[`../../docs/15-architectural-guardrails.md`](../../docs/15-architectural-guardrails.md);218checks are drawn from [`../../docs/09-component-architecture.md`](../../docs/09-component-architecture.md),219[`../../docs/08-design-tokens.md`](../../docs/08-design-tokens.md), and220[`../../docs/16-multitenant-performance.md`](../../docs/16-multitenant-performance.md).