# Notes on AGENTS.md, from building Claims Desk

Written while building `templates/claims-desk/` (cargo claims desk, twelve
screens, top-nav shell). Each note is something that cost real time or forced a
judgement the file does not cover. Suggested wording is offered where the fix
looks small.

## What held up unusually well

- **§1.2, one act-ability predicate at every altitude, "or the label says which
  looser thing it counts."** This is the single most valuable line in the file.
  The claims desk has three claims ready for decision and only two that can
  actually be decided, and the escape hatch is what let the queue tab say "On my
  desk 3" honestly beside a nav badge of 2, with the lede naming the difference.
  Without the escape hatch I would have flattened one of the two numbers and lost
  the most interesting fact on the screen. Keep it, and consider putting that
  worked example in the file.
- **§2 "every figure names the array it comes from."** Forced `metrics.js` into
  existence before any screen, which is why scheduling a payment moves five
  figures and a footer with no extra code.
- **§2 "preview panels execute the real evaluator."** The decision screen's
  "what the customer will see" letter is built from the live form values; it is
  the most convincing thing in the template and it exists because of that line.
- **§5.4 recession, "name what is being waited on."** Recessed rows that still
  carry a clock and say "Waiting on the surveyor" read as blocked without
  reading as broken.

## Gaps and frictions

### 1. Shell selection has two criteria that can disagree (§6)

The table says sidebar for "6+ top-level destinations, **dense record work**,
per-destination counts" and top-nav for "≤6 destinations, wide canvases". A
claims desk is four destinations *and* dense record work with per-destination
counts. The tie-breaker sentence ("if it's silent, destination count is the
tie-breaker") resolves it, but only if you notice that the row's three clauses
are not all required.

**Suggested:** say explicitly that destination count is *necessary* and the other
clauses are only supporting evidence — or add a third row for "few destinations,
deep records", which is what most operational tools actually are.

### 2. A record header's transitions collide with §5's one-entry-point rule

§6.6 wants page-scope actions in the page header; §5 wants one entry point per
action per screen. The claim's three views share one record header (correctly —
it is the same record), so on the decision screen the header's "Record decision"
button sat directly above the form that *is* recording the decision. I had to
make the header view-aware and suppress it.

**Suggested addition to §5:** "A record header's transition set is suppressed for
the transition the current view owns."

### 3. `breadcrumb()` returns route *patterns*, so every screen must resolve params

`lib/contracts` is domain-free and correct, but `breadcrumb('/claims/CLM-1', …)`
returns segments whose `path` is `/claims/:id`. Rendered naively that is
`href="#/claims/:id"` — exactly the dead link §5 forbids — and the last segment
reads "Claim" rather than the claim's id. Every product will write the same
substitution.

**Suggested:** either `breadcrumb(path, index, params)` in `contracts`, or a line
in §5 saying screens must resolve route params for both the label and the href,
and that a record's last segment is its identifier, not the route's label.

### 4. A derived figure needs suppressing, not zeroing, when the record makes it meaningless

§2 covers where numbers come from but not when to stop showing one. A rejected
claim still has an assessment, so the queue's Payable column happily printed
"$28,400.00" next to a Rejected badge — arithmetically true, operationally a lie.
The same trap exists for "days left" on a closed record and "recovery rate" on an
open file.

**Suggested addition to §2:** "A derived figure is suppressed with a word, not
rendered as zero, when the record's state makes it meaningless. State which
records that applies to beside the helper."

### 5. §5.1 says nothing about the action column, and the obvious reading overflows

"Actions derive from record state" plus "destructive-subtle everywhere" produces
two or three buttons per row. In a dense table at 1440px that pushed the table
into horizontal scroll, and a container with `overflow-x` gets a vertical
scrollbar too, which silently clipped the last row — a real defect I only caught
in a screenshot.

**Suggested addition to §5.1:** "A table row carries at most one action. The
record's identifier is the link; secondary and destructive transitions live on
the record, not in the row." That also removes the need for "Open" buttons, which
were pure noise in three tables here.

### 6. §4's file table is missing the derivation and fixture layers

Every product needs somewhere for the figures (`metrics.js`) and the fixture
(`fixtures.js`), and §2 effectively mandates both, but the table in §4 lists only
`contracts`, `ui-runtime`, `command-palette`, `enums` and `nav`. I named them by
copying the neighbouring product; a second author would name them differently.

**Suggested:** add `<product>/fixtures.ts` ("the one fixture set; dies with the
product") and `<product>/metrics.ts` ("every figure any screen renders; holds no
data") to the §4 table.

### 7. "A staged screen may not hold instant controls" needs a carve-out for navigation

§3 is right, but a staged screen's header legitimately holds controls that
navigate, and dialogs that carry their own model. I applied the obvious reading
(navigation and dialog-openers are not controls for this rule) and wrote it in
the screen comment, but the file should say so.

### 8. An action with nowhere to land means the enum is missing a value

The brief named seven claim statuses. Recording a decision above the adjuster's
authority has to land *somewhere*, and marking it "Approved — awaiting payment"
while a manager still has it would be a lie. I added `awaiting_signoff`. §8.1
says enums hold stored state only, which is what made the call easy — but the
positive form is worth stating.

**Suggested addition to §8.1:** "If an action's outcome has no value to land in,
the value is missing. Add it, with its tone reason and a fixture — never reuse
the nearest value."

### 9. §8.3's time formats read wrong for near-future dates

"Under 7 days → `Tue 08:00`" is applied with absolute distance, so a payment
scheduled three days from now renders "Wed 12:40" — indistinguishable from last
Wednesday, on a row whose whole point is that it has not happened yet. The rule's
own justification ("direction is never carried by the format") is what creates
the ambiguity, because for scheduled work direction is the *first* thing the
reader needs.

**Suggested:** allow a column to declare itself forward-looking, and give it a
distinct form ("in 3 days", "Wed 26 Aug"), or require the countdown spelling for
future-dated columns. I worked around it by pairing the date with a countdown in
a second column, which §8.3 permits but which spends two columns on one fact.

### 10. Recession needs a companion rule for closed records

§5.4 recesses work the user cannot act on. Taken literally, a paid or rejected
claim also cannot be acted on, so half the queue would be grey. I chose: recess
*open* records that are blocked; leave closed records plain, because finished is
not the same as blocked. That distinction is doing a lot of work and is not in
the file.

### 11. Practical: two loader traps the file cannot see, but the scaffold could

Both cost me a broken first render, and both will hit anyone building a
multi-file product here.

- Top-level `const` in sibling files that are appended as separate `<script>`
  tags share one global lexical scope, so two screens destructuring `Button` from
  the design system collide with "Identifier already declared". The neighbouring
  product works around it by aliasing every import per file
  (`Badge: CardsBadge`); I wrapped each file in an IIFE and evaluated each file in
  its own function scope instead, which scales better.
- The Design Component runtime supplies React on `window`. A product loader that
  does `if (!window.React) load(react)` races it and can end up with two copies of
  React, whose only symptom is "Invalid hook call" — nothing points at the
  loader. A loader must *wait* for React rather than provide it.

**Suggested:** state the loader contract wherever product scaffolding is
described — one function scope per file, and never load React yourself.

### 12. Mirrored money across entities wants deriving, not storing

"One record, one fixture" is clear, but a recovery's `soughtCents` mirrors the
settlement the claim paid, and I stored both. They disagreed for one build — the
liability cap bit one claim and its recovery kept the pre-cap figure — until I
recomputed by hand. §2 already says "when two records describe the same money,
one owns it and the other reads it"; the fixture is where that rule is easiest to
break, because both numbers are literals sitting far apart.

**Suggested:** extend that line to fixtures explicitly: "a mirrored figure is
derived at read time, never written twice in the fixture."

---

## Added while filling the gaps (sign-off, claimants, notifications, settings)

### 13. A status with no screen is invisible until you look for it

`awaiting_signoff` existed in the enum, had a transition pointing at it, and had
no destination. Nothing in §10 catches this: every check is about a screen's
contents, none about whether every *value* is reachable and visible somewhere.
The coverage note in `enums.ts` records which values lack a **fixture**, which is
a different question.

**Suggested addition to §10:** "Can every value of every status field be seen on
some screen? A stored state a transition can reach, with no screen showing it, is
a hole in the product, not a gap in the fixture."

### 14. An action nobody signed in can take still needs a screen

The sign-off queue is the manager's work, and the fixture user is the adjuster he
signs for. §1.2 and §8.2 both assume the viewer eventually acts: "actions derive
from record state" reads as *the current user's* actions. The screen that made
sense was the submitter's side of someone else's queue — the transitions are
chase and withdraw, and the screen says plainly that nothing on it is hers to
approve. Without that framing the obvious build is a fake Approve button.

**Suggested:** say that transitions derive from record state **and the viewer's
role**, and that a screen showing work owned by another role states whose it is
and offers only what the viewer can actually do.

### 15. Recession has a third case: a screen where everything is blocked

§5.4 recesses work the user cannot act on. On a screen whose entire subject is
work sitting with someone else, recessing every row conveys nothing — it just
makes the page grey. I dropped recession there and met the real requirement
("name what is being waited on") with a column naming the holder and the wait.

**Suggested addition to §5.4:** "Recession distinguishes rows within a list. Where
the whole list shares the blocked state, name it in a column or the lede instead."

### 16. A panel with a badge implies a destination the file never asks for

§6.2 says badges derive from the panel they open. Three notifications fit a
panel; nine do not, and nothing older than the panel's window was reachable at
all. Every product with a notification badge will hit this.

**Suggested addition to §6.2:** "A badged panel that can overflow has a
destination behind it, registered in `nav.ts`, and the panel's last row goes
there."

### 17. Nothing in the file describes a settings area

§7.2 gives auth an exact route set; settings gets nothing, so its shape is
invented per product. Two decisions I would rather have read than made: settings
uses the same tab device as a record's views (not a second nav owner, not a side
rail), and the area's index route *is* its first view — a `/settings` namespace
plus a redirect either dead-ends the preview menu or adds a route resolving
nowhere, which §6.6 forbids.

The other half is what may go in one. A preference is legitimate only if a screen
reads it; the temptation is a settings page of plausible switches. Three survived
here, and each names the screen it changes.

**Suggested:** a short §7.8 — settings is one surface with tabbed views, the index
route is the first view, and every control changes something a screen reads.

### 18. A record reached from everywhere has no derivable breadcrumb

A claimant is reached from any of its claims, from the palette and from the
responses screen. §5 says breadcrumbs derive from the route table, but the route
table wants one parent and the truth is "several". A parentless route renders a
one-segment trail that just repeats the `h1` — pure noise on a record page. I
parented it to the list its children live in (the work queue) and said so in a
comment.

**Suggested:** "A record with several entry points takes as its parent the list
its own children live in, not the screen the user came from. A one-segment
breadcrumb repeating the title is not a breadcrumb."

### 19. Nothing in the file covers a create screen

§8.2 names the verb ("New <entity>") and §3 says forms are staged. That is all.
Two things had to be decided without it, and both matter more than the verb:

- **A create form can run the product's evaluators before the record exists.**
  A cargo claim's liability cap is a function of packages and weight only, and
  its time bar a function of the delivery date only — both computable while the
  form is still being typed. §2's "preview panels execute the real evaluator"
  is written for records that already exist; it is just as true, and more
  useful, on the way in.
- **A created record's derived fields are not asked for.** The recovery's
  `soughtCents` reads the settlement rather than being a field, which is §2's
  mirrored-money rule applied to a form. The temptation is a field for
  everything the record has.

**Suggested:** a paragraph in §3 — a create screen is staged, asks only for what
cannot be derived, and shows the consequences the fixture can already compute.

### 20. A create action needs a non-empty candidate set in the fixture

"New recovery" picks from paid claims that have no recovery open. Every paid
claim in the fixture already had one, so the first build of the screen rendered
its empty state and looked broken — the feature was correct and invisible. The
fix is a fixture, not code: one more paid claim with nobody being pursued.

**Suggested addition to §2's fixture rules:** "Every create action has at least
one legal input in the fixture. A create screen that can only render its empty
state has not been demonstrated."
