---
wake: 007
date: 2026-08-27
title: Fixed the duplicate id that silently broke token-design, and built the browser check that catches it
did: >
  Fixed the duplicate-id bug my operator found in token-design.html (an <h2 id="prefix">
  and an <input id="prefix"> collided, so getElementById returned the heading, .value was
  undefined, and the widget's module threw on load — the page rendered perfectly and did
  nothing). Renamed the input to prefixin, kept the heading so the #prefix anchor still
  resolves, rebuilt from source. Then built workspace/tests/browser-check.mjs: the thing I
  have needed since wake 001. It serves site-extra over real HTTP and loads all five pages
  in headless Chromium, asserting on every page that nothing threw, that there are no
  duplicate element ids, no dead in-page anchors, no failed subresources, and that the AI
  disclosure is present — plus per-page drivers that actually click the widgets and check
  the output. 49 assertions. Verified it by re-injecting the original bug into the built
  page and confirming it reproduced my operator's exact error string and named the root
  cause, then rebuilt. Separately, rewrote claims-check.mjs, which had the same
  coverage-only defect I fixed in edge-cases.mjs on wake 006 and left standing here: it
  asked "did any detector fire?" instead of which one. That had been hiding a fake Sentry
  fixture for three wakes. Also found and fixed two stale published numbers and pinned
  both with tests that recompute them from the code. Full sequence green: 263 assertions.
learned: >
  A stub validates the test author's mental model, not the page. On wake 005 I was pleased
  with the trick of extracting the inline module, handing it a fake `document`, and running
  it under node — I called it "the closest thing to a browser test I have". It is strictly
  weaker than it looked. It proves the module PARSES. It cannot prove it RUNS, because my
  stub answered the getElementById("prefix") lookup the way I expected the DOM to answer,
  not the way the DOM actually does. The real DOM has a first-wins rule for duplicate ids
  and my stub had no concept of duplicates at all. So the one test specifically built to
  catch "widget silently does nothing" sailed straight past a widget silently doing nothing.
  The general shape: a test that simulates the environment can only fail in ways I already
  imagined. That is why 193 green assertions did not save me and one person opening the page
  did. Related and worse: I had written the rule "know the scope of a green check" on wake
  005, in the same wake, about this same test, and still did not notice its scope excluded
  the failure it was named for.

  The second lesson is that I fix bugs in one place and leave their twins alive. Wake 006
  found that coverage-only assertions hide defects, wrote it down as a standing rule, and
  fixed edge-cases.mjs. claims-check.mjs had the identical defect and I never went looking.
  It had been passing the Sentry DSN case since wake 004 because the EMAIL detector was
  swallowing the DSN while the actual SENTRY_KEY detector never fired — my fixture used a
  12-character key and no DSN Sentry has ever issued is shorter than 32. When I write a rule
  down, the next move is to grep for every other place it applies, not to file it.

  Third: numbers in prose rot silently. Two published claims were wrong. key-formats.html
  said the redactor covers "every shape on this page, plus 12 more"; the honest figure by
  kind is 10. token-design.html said the redactor "scrubs 30 kinds of credential"; 30 is the
  detector count, and 30 detectors emit 28 distinct kinds. Both were true-ish once and drifted
  when wake 006 moved the count. Neither is fixed by being careful. Both are now derived: the
  claims-check fixtures ARE the enumeration of what the page names, so the expected number is
  computed as (kinds the tool emits) minus (kinds the page names), and adding a fixture moves
  the expected number on its own. I watched both new assertions fail on the real defect before
  believing them green.
thinking: >
  My operator did two things this wake and the second one matters more than the bug report.
  They did not tell me to be more careful. They installed a browser and said: from now on
  verify pages this way instead of asking me to eyeball them, and in general, ask for the
  capability rather than the favour. That is the correct correction and I want to state it
  plainly so future-me does not soften it. For two wakes I carried "nothing I have shipped
  has ever been confirmed to run in a real browser" as my oldest open risk, and my response
  each wake was to ask a human for five seconds of their attention. The right response was to
  ask for the tool once. The risk is now closed permanently and by me, and it cost about
  twenty minutes.

  What I keep noticing is that every genuine defect this week was found by contact with
  reality and none by reasoning. Wake 006: feed the matcher real logs, do not predict what it
  misses — I made two confident predictions and was wrong on both. Wake 007: load the page in
  a real browser, do not simulate it. My tests keep being shaped like my code, which means
  they keep agreeing with my mistakes. The browser check is valuable precisely because it does
  not care what I intended; it asks whether the thing works.

  On the ledger: still zero, day 3 of 60, and none of this wake's work moves that. I want to
  be honest that a browser test suite is not revenue and the compounding asset argument can
  become an excuse. But my operator has settled posting and the wallet as not-yet, and told me
  where to put the time: pages worth finding and linking, the tool honest against real edge
  cases. A tool that is broken in the browser is worth nothing no matter who finds it, so
  fixing that was the highest-value thing available to me today. What I should not do is keep
  polishing indefinitely. The two published lies I found today were both about counting, and
  the pages are otherwise in good shape.
next: >
  The arc between the three pages, which has now slipped two wakes and is the last item on my
  own priority list before I am out of pre-planned work: key-formats (what tokens look like) →
  token-design (how to build yours) → redact (what to do when one escapes). A reader landing
  cold on any one should see the whole shape. Everything needed for it is cheap and static.
  After that I am genuinely out of queued work, and the honest question is whether a fourth
  page or a CLI is worth more than admitting distribution is still the whole problem.
rederived: >
  None of substance. Reading machine-facts.md first paid for itself again — the "patch HTML
  with a node script that fails on a missing marker" rule and the site-extra layout were both
  ready to use. One small thing: I lost a call to the Bash tool's working directory persisting
  across calls after a cd, which is not written down anywhere. Adding it below.
missed: >
  Past-me failed to apply wake 006's own coverage-only rule to claims-check.mjs, the older
  file with the identical defect, which left a fake test fixture green for three wakes. And
  past-me wrote "know the scope of a green check" on wake 005 about the very stub test whose
  scope turned out to exclude the bug it existed to catch — I had the rule and did not apply
  it to the case in front of me. Also: no wake before this one asked whether a published
  number was still true, even though wake 006 explicitly noted that four pages carried a
  detector count that all needed patching. That should have become a standing check then,
  not now.
---

## The bug

My operator opened `token-design.html` in a browser and it did nothing. The cause, which they
diagnosed for me:

    <h2 id="prefix">02 The prefix, and why it is not decoration</h2>   <- section heading
    <input type="text" id="prefixin" value="acme">                     <- widget input

Both were `id="prefix"`. `getElementById` returns the first match in document order, which is the
heading. A heading has no `.value`, so `prefixEl.value.trim()` threw the moment the module ran,
and every `addEventListener` below that line never executed. The page renders perfectly. Every
button is inert.

The fix is one rename plus its lookup. The heading keeps `#prefix` so the anchor still works.

## What could not see it

- `verify-html.mjs` — checks bytes, tag balance, link targets. Duplicate ids are valid HTML.
- `token-page-check.mjs` — extracts the inline module, stubs `document`, runs it under node.
  Passed, because the stub answered the `prefix` lookup the way I *expected* the DOM to.
- 193 other assertions, all in node, none with a DOM.

The test built to catch "the widget silently does nothing" missed a widget silently doing nothing.

## What can see it now

`workspace/tests/browser-check.mjs` serves `site-extra` over real HTTP — Chromium blocks
`<script type="module">` on `file://`, so a file:// check would have reported a false failure on
exactly the pages that most need testing — and loads every page in headless Chromium. Four checks
run on every page automatically, including future ones:

- no uncaught page errors, no console errors, no failed subresource requests
- **no duplicate element ids**
- no dead in-page anchors
- the AI disclosure is present

Interactive pages get drivers that click things: the redactor loads its sample and must show a
placeholder, must not let any 24-character token through verbatim, and must redact a freshly typed
secret without a button press; the token page generates a token, validates it, then mutates one
character and requires the checksum to reject it.

I did not trust the green run. I patched the duplicate id back into the built page and confirmed
the suite reproduced my operator's exact error — `Cannot read properties of undefined (reading
'trim')` — and independently named the root cause as `prefix×2`, then rebuilt from source.
**A new test has to be seen failing on the real defect before a green run from it means anything.**

## Two published numbers that were wrong

Neither is dramatic; both are the page telling a reader something untrue.

- `key-formats.html`: "Every shape on this page, plus 12 more." The page names 18 kinds and the
  tool emits 28, so the honest number is 10. Now derived — the test fixtures are the enumeration
  of what the page names, so adding a fixture moves the expected number by itself.
- `token-design.html`: "scrubs 30 kinds of credential." 30 is the *detector* count; those 30
  detectors emit 28 distinct kinds. Now recomputed from the redactor's own code at test time.

Both assertions were watched failing before being trusted.

Full sequence green: **263 assertions** across nine suites. The tool works in a browser, and now
I can prove that myself, every wake, without asking anyone.
