What was actually verified, and what was not โ
- Ran: 2026-08-27, headless chromium at 1440x1000.
- Targets, both the operator's own running servers, neither restarted: the web app at
http://localhost:5173(step 1) and the admin + merchant portal athttp://localhost:3001(step 2), signed in as Agent Probe. - Harness and raw output:
harness/. Captures:screens/.
Step 1: the signed-in web survey โ
- 12 of 14 routes rendered and were measured. The two that did not are findings, not gaps:
#/activepaints a blank page and#/wavesredirects to#/. Full detail indesign-survey-web.md.
Step 2: the empty-value grammar โ
The result that matters: zero em dashes render on either portal. โ
| Route | Rendered em dashes |
|---|---|
/admin/system, /admin/config/services, /admin/merchants, /admin/venues, /admin/moderation, /admin/analytics/bigquery, /admin/financials | 0 |
/merchant/<id>/overview, /offers, /venues, /settings | 0 |
- 11 routes, both portals, one signed-in session. The probe walked every visible element and read its own text nodes, so this counts what a person SEES, not what a grep finds.
The constant path is confirmed with real data, on the screen the survey named. โ
screens/portal-services-cors.png: the Services & CORS "Prod URL" column renders sevenNOT_SEThyphens where the survey's A9 recorded em dashes. That is the highest-density instance in the portal and it is now correct.
The COMPONENT path was NOT observed live, and the reason is measured rather than assumed. โ
<NotSet />and<Unavailable />are data-dependent at every call site: a table cell with an empty field, a metric whose value is null. No such row exists in dev data today on the surfaces they were applied to.- Measured, not inferred: every visible row on
/admin/venuescarries an address and a source;/admin/merchants/all,/admin/merchants/applicationsand/admin/financialsrender no placeholder of any kind, before or after the change. - The only bare hyphen left on any admin page is the sidebar collapse indicator (
expanded ? '-' : '+'inAdminShell.jsx), which is a paired glyph and deliberately untouched. It showed up as "Merchants-" and "Analytics-" in the probe, which is what it is supposed to be. - A load-window probe was also run (150ms through 5000ms after navigation) to catch a placeholder rendering before data arrives. It found none, because the loading surfaces render skeletons rather than placeholders.
So the component markup is covered by tests, not by a screenshot. โ
- Five render tests in
EmptyValue.test.jsxassert the glyph by codepoint, thetitle, thesr-onlyphrase, the caller-supplied reason, and thatNotSetdoes NOT claim thecursor: helpaffordance it has nothing to explain with. - The admin production build generates
.sr-only(clip-path: inset(50%); position: absolute; 1px x 1px), so the label is hidden visually and available to assistive tech. Checked in the built CSS, because a Tailwind utility that is never scanned would have rendered the phrase visibly beside every dash on both portals. - This is the honest boundary: the sweep is verified live, the component's internals are verified by test. Seeding an empty field into shared dev data to force a render was not worth doing to other sessions.
The one instance the pass deliberately left is visible in the capture. โ
screens/portal-merchant-overview.pngshows the exact defect the survey described: "Unique visitors" renders a dash while "Draft", "Expired" and "Across venues" render zeros beside it, with nothing telling the reader which is which.- The probe confirms it is a bare U+2013 in a
<dd>, not wrapped:Overview.jsx's own literal, which this pass did not touch (it is a#320screen and another session had it open). - This is the single highest-value remaining application of step 2: two lines in one file, no visual change, and it gains the sentence that makes the dash mean "too quiet to report" rather than "nobody came".
Step 5, first three items: the header subtitle, the Overview empty value, the prose sweep โ
- Ran: 2026-08-27, headless chromium at 1440x1000, on
http://localhost:3001, the operator's own stack, nothing restarted or rebuilt. Vite HMR carried the source edits. - Post-login URL, every run:
http://localhost:3001/admin, and every route additionally recordedonLoginScreen: false. So none of the zeros below is the rate-limiter wearing a clean pass. - Harness:
harness/verify-step5-jobs.mjs. Raw output:harness/step5-verify.json. Captures:screens/j-a-*.png(admin) andscreens/j-m-*.png(merchant). - 15 routes, 10 admin and 5 merchant, one signed-in session per run.
PageHeader carries the subtitle in title on 13 of 15 routes, including all 5 merchant ones. โ
- The header bar measures 1130x68 on every one of the 15, unchanged, so the flush sidebar alignment the CSS comment protects is intact.
- The route the decision was about still clips, and now carries the text:
/admin/analytics/bigquery/exportat 452 of 513px,titlepresent and equal to the rendered subtitle. That is the mitigation working. It is not a fix for truncation, and the durable answer is still shorter copy. - Every other route measures
clientWidth == scrollWidth, so nothing else clips at 1440px, which matches the original 11-route measurement.
The 2 routes without a title are the ones that pass a NODE, not a string, and this is the guard behaving correctly. โ
| Route | What its subtitle is | Clipped |
|---|---|---|
/admin/system | A fragment: Infrastructure metrics and service status plus a conditional Cached (Ns ago) badge (SystemHealth.jsx:231) | no, 367/367 |
/admin/financials | Same shape (Financials.jsx:494) | no, 323/323 |
titletakes a string. Passing a React element would have rendered an object stringification into the tooltip, so the component declines rather than lying.- Neither clips, so the gap costs nothing today. Covering them needs a decision that was not made: either a second
subtitleTitleprop or reading the text back off a ref after mount. That is a fork, not an oversight, so it is left for whoever owns step 5's remaining items.
Merchant Overview's "Unique visitors" now says what its dash means. โ
- Read live off the running page: glyph
U+2013,cursor: help, and both thetitleand thesr-onlyphrase reading "Too quiet to report, not zero: counts from fewer than 3 unique visitors stay hidden to protect anonymity". - The floor is interpolated from
K_ANON_DISPLAY_FLOOR, which the file already imports, rather than typed as a literal, so it cannot drift from the server's actual gate. - Zero visual change, confirmed against
screens/portal-merchant-overview.png(before) andscreens/j-m-overview.png(after). The card still readsLanterns 30d: 11with a dash besideUnique visitors, next toDraft 0,Expired 0andAcross venues 1. The difference is only reachable, which is the point. - Nothing else on that screen was touched.
statValuestill serves its other seven call sites with the bare literal, deliberately: one generic reason attached to all of them would flatten meanings that genuinely differ.
Zero banned long dashes render on any of the 15 routes. โ
- The probe walks every visible element, reads its own text nodes, and also scans every
titleattribute, since job 1 deliberately puts copy there. It counts what a person sees, not what a grep finds. - The first run found 2 that a source grep could never have caught, both on
/admin/client-sdk, in the category descriptions for "User Profiles" and "Onboarding". They render fromapps/admin/src/generated/client-sdk.json, which is gitignored and therefore invisible to the ratchet linter, and they are authored ingenerate-client-sdk-docs.mjs'sCATEGORY_DESCRIPTIONSmap. Fixed at the authored source and regenerated; the second run reads 0. - This is the argument for driving the browser rather than trusting the linter. A gitignored build artifact is out of every static gate's scope and still lands in a user's face.
What blocked, and what it cost โ
- The portal sign-in rate limit ate one run.
/auth/admin/signinand/auth/merchant/signinshare one 10-per-15-minutes-per-IP bucket, and the operator's own portal session spends from it too. The second pass silently landed on the login screen and reported zeros for every route, which reads exactly like a working page with no findings. - The tell:
AFTER_LOGIN http://localhost:3001/instead of/admin. Every harness in this folder logs the post-login URL for that reason. Budget one sign-in per run and reuse the session.