# Notes from building the Recruitment Desk

Friction worth recording, in the order it came up. Nothing here is a complaint about the
rules; it is the places where a rule was silent and I had to decide.

## 1. A reader-driven fold is a third mode, and §6.5 only knows about two

`AGENTS.md` §6.5 describes the sidebar collapsing by **width**: expanded, rail, drawer.
The brief here asked for a fold on a button, which is a different thing — a preference,
not a breakpoint. Both have to exist at once, and only one of them can win.

What I settled on, and would suggest as the general answer: the viewport decides what is
*possible*, the reader decides between the states where both are possible.

```
mode = viewport === "drawer" ? "drawer" : fold === true ? "rail" : fold === false ? "expanded" : viewport
```

The preference has to be tri-state — untouched, folded, unfolded — or the reader who lands
at rail width has no inverse and is stuck in an icon sidebar. That was the bug in my first
pass, and it is the whole point of the feature.

The consequence worth stating in the rules: **the fold control is only offered where there
is something to fold to.** At drawer width it would fight the menu button, so it is not rendered there — §6.3 licenses a collapse control in the
sidebar footer ("the footer zone holds only account, help or collapse") — the only place in
the file that anticipates this feature at all — but the footer turned out to be the wrong
home for it in use: the reader wants the width back while reading the content, and their
eye is on the content side. It ended up as a chevron in the toolbar's leading slot, which
§6.1 covers as a utility action.

**Suggested, for §6.5:** one line saying that a user-toggled collapse is legal, that it
composes with the width rule rather than replacing it, and that its control may sit in the
sidebar footer *or* the toolbar's leading slot — but disappears where the rail is forced.

## 2. A signature component needs two declared geometries, not one

The stepper appears at two scales: full on the record, and as a row of pips in table cells
and on board cards. The responsive rule that turns the full stepper into a vertical list
below 1100px broke every mini one — twelve pips stacked inside a table cell — because the
media query targeted `.rd-stepper` and the mini variant is the same class.

It is an obvious bug in hindsight and a subtle one in the file: §9 asks for one signature
per product and §6.5 asks for one reflow declaration, and the two together imply that a
signature has exactly one geometry. It usually has two, and the compact one must be
excluded from the reflow by name.

## 3. "Evidence" is derivable, but only if the fixture is shaped for it

§2 wants figures to name the array they came from. A stepper is not a figure, but the same
rule bites: a completed step wants to say *what* completed it. Nothing stores per-step
timestamps, and adding a `stageLog` would have been a stored field for something derivable.

So `stepEvidence` reads the sub-records instead — the screening call from `interviews`, the
returned exercise from `documents`, the last scorecard, the references, the offer letter —
and returns `null` where the fixture cannot support a claim, in which case the step falls
back to saying what it is *for*. That worked, but it only worked because the sub-records
were designed with it in mind. A fixture written first and a stepper added later would have
had nothing to read.

## 4. Per-stage service levels change what "one predicate" means

§2 asks that the row, the section count and the nav badge call one act-ability predicate.
Here the interesting predicate is not act-ability (that one is easy: `awaitsUs`) but
*lateness*, and lateness is per stage — three days at applied, ten at checks. A single
`isStalled(application, now)` that looks its own allowance up in `RD_POLICY.stageDays` keeps
one predicate honest, but every label then has to avoid implying one shared number. The
copy says "over their service level", never "over 5 days".

## 5. The data note: 24 applications is enough to make a funnel lie

With this few records, a conversion rate between two steps is a ratio of single digits.
Rather than dress that up, `/insights` lists the offers one by one under the funnel and the
funnel footer computes where the biggest drop actually is instead of asserting a benchmark.
If a rule wanted adding, it would be: **below some sample size, list rather than average** —
the checkability §2 asks for is better served by five rows than by one percentage.

## 6. Two value shapes, one helper name, and a silent failure

§4 names `labelOf` for classification lists and `StatusBadge` for status fields, and it is
right about both — but it does not say that the two shapes differ. A status value is
`{id, label, tone}`; a classification value is `{value, label}`. `labelOf` matches on
`.value`, so handed a status enum it finds nothing and returns the id it was given. No
error, no warning: `/compliance` printed "DBS not_started" and "right to work
needs_sponsorship" at the reader on five of its first six rows, and it survived a build
because the only value ever read in early passes was `verified`, which happens to look
like a label.

It became visible only when a second fix routed the advance gate through the same list.
The template now has one `statusWords(values, id)` over `statusValue`, and the enums
header states the shape rule.

**Suggested, for §4:** one line giving the two shapes explicitly, and saying that reading
a status field with `labelOf` fails silently rather than throwing. A rule that can only be
broken invisibly is worth naming.

## 7. Not every check is owed by every record

The pre-employment gate looked like three symmetrical checks — right to work, DBS,
registration — so all three read off the application, and `clearToStart` required a
registration on everybody. It is right for a nurse and wrong for a ward clerk: whether a
registration is owed at all is a property of the **seat**, and the fixture had no field
saying so, so a ward clerk with an accepted offer sat on `/compliance` as a blocked hire
with "no registration on file".

§2 covers this for figures — a qualifier decomposes its own figure's set — but a predicate
has the same failure mode: it can be applied to a set wider than the one it is true of.
The fix is `requisition.registration`, and the predicates that read it take the
requisition rather than assuming.

**Suggested, for §2 or §5.4:** a predicate names the set it is true of, and a check that
does not apply to a record is absent from that record rather than failed on it — the
predicate equivalent of "omit an action when the record's state makes it meaningless".
And the sharper half of the same lesson: recession must read the **same** predicate the
counts read. Recession keyed on "clear to start" while urgency keyed on "has a blocker"
recessed the one row on the screen carrying a warning badge and a live action, because a
registration in date today and renewing in seventeen days satisfied both at once.

## 8. Host height and `100dvh`

Unrelated to the rules, but it cost me twenty minutes: inside the design-component host the
page is given its full content height, so the `height: 100dvh; overflow: hidden` shell
resolves to the content height and nothing scrolls internally. The shell is still correct —
in a real viewport the content column is the scroll owner — but any check of "one scroll
owner" has to be done at a fixed viewport height, not in the auto-sized preview.
