J6: a player finds out what happened — recap, highlights, storyline, wiki (#726, #729) #770

Merged
claude-bot merged 4 commits from build/lane3-j6 into main 2026-09-23 14:54:37 +00:00
Collaborator

Lane 3's acceptance gate, and the one piece of it nobody had written. #729 requires "J1 … and J6 (Highlights, Storyline) pass on desktop, on a phone and keyboard-only … with the axe scan clean"; e2e/ had j0, j0b, j1 and j2.

sh scripts/journeys.sh (whole suite, no grep): 24 passed, 6 skipped — the six are the two pre-existing test.skip placeholders × three projects. Frontend unit suite 1591 / 127 files. a11y-baseline.js is still {} and no run printed a tolerated line.

What it walks

One sign-in as Priya, four acts, read-only.

The recap. Home's card carries Last time and the first sentence; she opens the campaign by the card's own Open link rather than an address — that is the act's claim, that a player who missed the evening finds out what happened without being told where to look. On This week the h1 is the session's title (no numbering, #760) and "Previously on" carries the whole recap, asserted via its last sentence, which the card's one-liner does not contain.

SE-006's "would not load" state is walked for real — page.route aborts the cold-open request, the page reloads, and the journey asserts the banner, the retry, the absence of Hide, and that the h1 and About the campaign survived. That is a SectionBoundary doing its job in a browser rather than in jsdom, which nothing else in the suite has ever shown. The absent state is deliberately not walked and says so in the spec: it renders nothing, and reaching it would need a second campaign existing only to have no history. ThisWeek.test.jsx holds it.

Highlights. Two groups, one per played evening; every line asserted under its own group and absent from the other. Then the rule the page exists for: the group's date is read out of the <h2> and asserted to be non-empty, not today's (the quotes were filed minutes earlier by the seed — printing created_at is precisely the mistake) and different between groups. The filter is exercised on a speaker who spoke in both evenings (both groups must stand) and one who spoke in one, and the "Said by" choices must still number all four afterwards (CA-228).

Storyline. D47 in one assertion: exactly two level-2 headings — the story and the timeline, not four things. The timeline's first row carries the same date string Highlights printed for that session, so two surfaces are pinned to agreeing about one evening, which they would not if either had reached for created_at.

The visibility leak is asserted three ways — the gm and player_private titles and details absent from the page; a direct GET /timeline with Priya's own cookies asserted not to contain them while it does contain the two public ones, so the assertion cannot pass vacuously; and again on the wiki article.

The wiki is walked, not stubbed — Contents → front → search → entry → its timeline — and marked in the spec as lane 8's seam with a note on what lane 8 must keep true.

Eight axe scans per project, 24 a run, all clean

Home is scanned even though J2 scans it in the same run: the rule is a scan on every page a journey reaches, and a journey leaning on another's coverage goes quiet the day that one stops looking.

Findings in already-merged code

1. A WCAG 2.2 AA 1.4.3 failure on the wiki front page — fixed here. "Last updated …" was text-gray-400 dark:text-gray-600: #99a1af on white at 12px, 2.6:1 against a 4.5:1 minimum. Now gray-500/gray-400 (4.84:1 light).

It had never fired because the line only draws when a storyline has a generated_at, and until J6 seeded a story no e2e stack had ever had one. The journey's own fixture is what exposed it.

The pattern is systemic: that same pair is on 16 more faint lines across 7 files, all on surfaces lanes 4, 5 and 8 still have to rebuild. Only the one a journey walks is fixed here; the rest is filed separately, because a sixteen-place sweep belongs in a deliberate commit and not inside a journeys PR.

2. a11y-baseline.js cannot describe a campaign-scoped page at all — it looks entries up as an exact A11Y_BASELINE[pathname] key, and every address inside a campaign carries a per-run UUID. From lane 3 onwards the only key that can ever match is "*", which quietens a rule on every page in the suite. So tolerating the contrast failure above was not actually an option; fixing it was the only move available. Filed.

3. The wiki is a dead end on a phone. The category rail is hidden md:flex, and on an article it is both that page's only navigation surface and where its copy of the search lives. Below 768px, once a player is on an entry there is no wiki navigation and no wiki search — the only way onward is the browser's Back button. That is a step strictly harder on a phone than with a mouse, which is a regression against the brief's own bar. J6 passes on all three projects because it uses the front page's search, which #396 moved into the main column. Named on lane 8's issue.

4. The generated story renders its chapter separators as literal --- paragraphs — update_campaign_storyline joins chapters with "\n\n---\n\n" and chapters() treats anything that is not a Chapter N — heading as a paragraph. J6 deliberately does not assert this: asserting current-but-wrong output cements it. Filed.

5. identities.json carried the GM's retired surname in "sub": "e2e-dana-okafor" — in the file the README calls "the one place their names are written down", which is exactly why it must not be wrong there. Fixed in its own commit. Safe: make journeys starts from down -v and auth.py makes the first user admin regardless of sub.

6. The suite is now visibly paying the sign-in limiter. Eighteen sign-ins a run against 10/minute per IP. signInAs waits the 429 out so every run is green, but one test per run takes ~23s instead of ~3s, and which test moves between runs — the signature of a window being waited out rather than of a slow page. Worth saying before somebody diagnoses it as a slow page. Noted on #726.

7. The axe gate only ever sees light mode — Chromium's default prefers-color-scheme. The brief makes both themes first-class on every page, so the dark half of the contrast fix above is computed, not measured. Noted on #726 as a decision for whoever owns the harness.

Choices a reviewer should weigh

  • J6 is read-only and its fiction is seeded. The three projects share one database and run in sequence, so a spec that approved a recap would take that state from the next project. Same split the README already draws for J0 and J2, and it keeps J6 to one sign-in.
  • The story is not written by the seed. Setting a summary fans out to update_campaign_storyline, which rebuilds the body from every summarised session with no model involved, so the seed waits for the worker. A hand-written body would set manually_edited_at and take that path out of the run.
  • A second played session was added — without it neither grouping nor ordering is testable, since one group is always correctly grouped and always correctly ordered.
  • No new dependency on the wall clock: dates are asserted as relationships (not today, not each other, newest first), never as literals, and "today" is computed in the browser because a zone's marker is locale data.

🤖 Generated with Claude Code

Lane 3's **acceptance gate**, and the one piece of it nobody had written. #729 requires *"J1 … and J6 (Highlights, Storyline) pass on desktop, on a phone and keyboard-only … with the axe scan clean"*; `e2e/` had j0, j0b, j1 and j2. `sh scripts/journeys.sh` (whole suite, no grep): **24 passed, 6 skipped** — the six are the two pre-existing `test.skip` placeholders × three projects. Frontend unit suite 1591 / 127 files. **`a11y-baseline.js` is still `{}`** and no run printed a `tolerated` line. ## What it walks One sign-in as Priya, four acts, read-only. **The recap.** Home's card carries Last time and the *first sentence*; she opens the campaign by the card's own **Open** link rather than an address — that is the act's claim, that a player who missed the evening finds out what happened without being told where to look. On This week the h1 is the session's title (no numbering, #760) and "Previously on" carries the *whole* recap, asserted via its last sentence, which the card's one-liner does not contain. `SE-006`'s **"would not load" state is walked for real** — `page.route` aborts the cold-open request, the page reloads, and the journey asserts the banner, the retry, the absence of Hide, and that the h1 and About the campaign *survived*. That is a `SectionBoundary` doing its job in a browser rather than in jsdom, which nothing else in the suite has ever shown. The **absent** state is deliberately not walked and says so in the spec: it renders nothing, and reaching it would need a second campaign existing only to have no history. `ThisWeek.test.jsx` holds it. **Highlights.** Two groups, one per played evening; every line asserted under its own group *and absent from the other*. Then the rule the page exists for: the group's date is read out of the `<h2>` and asserted to be non-empty, **not today's** (the quotes were filed minutes earlier by the seed — printing `created_at` is precisely the mistake) and different between groups. The filter is exercised on a speaker who spoke in both evenings (both groups must stand) and one who spoke in one, and the "Said by" choices must **still number all four** afterwards (`CA-228`). **Storyline.** D47 in one assertion: exactly **two** level-2 headings — the story and the timeline, not four things. The timeline's first row carries *the same date string Highlights printed for that session*, so two surfaces are pinned to agreeing about one evening, which they would not if either had reached for `created_at`. **The visibility leak is asserted three ways** — the `gm` and `player_private` titles *and* details absent from the page; a direct `GET /timeline` with Priya's own cookies asserted not to contain them **while it does contain the two public ones**, so the assertion cannot pass vacuously; and again on the wiki article. **The wiki** is walked, not stubbed — Contents → front → search → entry → its timeline — and marked in the spec as lane 8's seam with a note on what lane 8 must keep true. ## Eight axe scans per project, 24 a run, all clean Home is scanned even though J2 scans it in the same run: the rule is a scan on every page a journey *reaches*, and a journey leaning on another's coverage goes quiet the day that one stops looking. ## Findings in already-merged code **1. A WCAG 2.2 AA 1.4.3 failure on the wiki front page — fixed here.** "Last updated …" was `text-gray-400 dark:text-gray-600`: #99a1af on white at 12px, **2.6:1** against a 4.5:1 minimum. Now `gray-500`/`gray-400` (4.84:1 light). It had never fired because the line only draws when a storyline has a `generated_at`, and until J6 seeded a story **no e2e stack had ever had one**. The journey's own fixture is what exposed it. **The pattern is systemic**: that same pair is on **16 more faint lines across 7 files**, all on surfaces lanes 4, 5 and 8 still have to rebuild. Only the one a journey walks is fixed here; the rest is filed separately, because a sixteen-place sweep belongs in a deliberate commit and not inside a journeys PR. **2. `a11y-baseline.js` cannot describe a campaign-scoped page at all** — it looks entries up as an exact `A11Y_BASELINE[pathname]` key, and every address inside a campaign carries a per-run UUID. From lane 3 onwards the only key that can ever match is `"*"`, which quietens a rule on *every* page in the suite. So tolerating the contrast failure above was not actually an option; fixing it was the only move available. Filed. **3. The wiki is a dead end on a phone.** The category rail is `hidden md:flex`, and on an **article** it is both that page's only navigation surface and where its copy of the search lives. Below 768px, once a player is on an entry there is no wiki navigation and no wiki search — the only way onward is the browser's Back button. That is a step strictly harder on a phone than with a mouse, which is a regression against the brief's own bar. J6 passes on all three projects because it uses the *front* page's search, which #396 moved into the main column. Named on lane 8's issue. **4. The generated story renders its chapter separators as literal `---` paragraphs** — `update_campaign_storyline` joins chapters with `"\n\n---\n\n"` and `chapters()` treats anything that is not a `Chapter N —` heading as a paragraph. J6 deliberately does **not** assert this: asserting current-but-wrong output cements it. Filed. **5. `identities.json` carried the GM's retired surname** in `"sub": "e2e-dana-okafor"` — in the file the README calls "the one place their names are written down", which is exactly why it must not be wrong there. Fixed in its own commit. Safe: `make journeys` starts from `down -v` and `auth.py` makes the first user admin regardless of `sub`. **6. The suite is now visibly paying the sign-in limiter.** Eighteen sign-ins a run against 10/minute per IP. `signInAs` waits the 429 out so every run is green, but one test per run takes ~23s instead of ~3s, and *which* test moves between runs — the signature of a window being waited out rather than of a slow page. Worth saying before somebody diagnoses it as a slow page. Noted on #726. **7. The axe gate only ever sees light mode** — Chromium's default `prefers-color-scheme`. The brief makes both themes first-class on every page, so the dark half of the contrast fix above is computed, not measured. Noted on #726 as a decision for whoever owns the harness. ## Choices a reviewer should weigh - **J6 is read-only and its fiction is seeded.** The three projects share one database and run in sequence, so a spec that approved a recap would take that state from the next project. Same split the README already draws for J0 and J2, and it keeps J6 to one sign-in. - **The story is not written by the seed.** Setting a summary fans out to `update_campaign_storyline`, which rebuilds the body from every summarised session with no model involved, so the seed waits for the worker. A hand-written body would set `manually_edited_at` and take that path out of the run. - **A second played session was added** — without it neither grouping nor ordering is testable, since one group is always correctly grouped and always correctly ordered. - **No new dependency on the wall clock**: dates are asserted as relationships (not today, not each other, newest first), never as literals, and "today" is computed in the browser because a zone's marker is locale data. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Lane 3's acceptance gate, and the sixth of the brief's seven journeys.
Priya signs in once and reads her way through a week she missed, on a
desktop, on an emulated Pixel 7 and with no pointer at all, with an axe
scan on every page she reaches:

1. the recap — Home's card already carries last time's first line, the
   campaign's own page carries the whole of it under "Previously on"
   (SE-006), and the door to the session names the same evening. The
   second of that section's three states is walked for real by failing
   the cold-open request: it says so, offers a retry, and the rest of
   the page is untouched. The third renders nothing at all, so there is
   no surface to walk; ThisWeek.test.jsx holds it.
2. Highlights — two groups under the two evenings, each dated from its
   session rather than from the day the line was kept, and a speaker
   filter that leaves the grouping standing and whose choices do not
   shrink as it filters (CA-228).
3. Storyline — two things and not four (D47): the story in chapters,
   and one timeline over sessions, a milestone and the wiki's own
   events, filterable by kind, read-only for a player all the way down.
   **A player is handed neither a `gm` timeline event nor a
   `player_private` one**, asserted on the page, on the article the
   events belong to, and in the answer the server sends.
4. the wiki — lane 8's page, walked against what is there today rather
   than stubbed, and marked as the seam it is.

The journey mutates nothing. What it reads is seeded as scenery,
because the three projects share one database and run one after
another: the seed gains a second played session (one group is always
correctly grouped, so neither grouping nor ordering is testable against
one), two recaps written with the GM's own endpoint, four highlights
published by approving the sessions, a milestone, and a wiki entry
carrying four timeline events — two of them seeded so that they can be
absent. The story is not written by the seed at all: setting a summary
fans out to update_campaign_storyline, which rebuilds it from every
summarised session with no model involved, so the seed waits for the
worker instead of hand-writing a body that would mark the storyline
edited and take that path out of the run.

The shell's Contents gains a fixture of its own — openContents/goVia —
because it is a rail on a desktop and a sheet behind a button on a
phone, and every lane from here on has to walk it.

One defect fixed in a page this found: the wiki front page's "Last
updated" line was #99a1af on white at 12px, 2.6:1 against WCAG 2.2 AA's
4.5:1, and about the same in dark. The journeys are the first thing
ever to put a generated story in front of it. It could not have been
baselined even if that were wanted — a11y-baseline.js keys entries by
exact pathname and every address inside a campaign carries a UUID that
changes with the run — so the only key that would have matched is "*".
a11y-baseline.js stays {}.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fix(e2e): the GM's surname, in the one place the names are written down (#726)
Some checks failed
CI / Bot/backend version sync (pull_request) Successful in 1m0s
CI / Backend lint (ruff) (pull_request) Failing after 1m10s
CI / Feedback site tests (pull_request) Successful in 1m41s
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 2m7s
CI / Bot tests and audit (pull_request) Successful in 3m4s
CI / Docker image build (pull_request) Successful in 3m42s
CI / Frontend tests, audit, and build (pull_request) Successful in 4m7s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Failing after 5m9s
CI / Backend migration, tests, and audit (pull_request) Successful in 10m55s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 13m17s
f8b70be564
`identities.json`'s Dana carried `"sub": "e2e-dana-okafor"` — a surname this
product retired. The README calls this file "the one place their names are
written down", which is exactly why it is the one place it must not be wrong.

Safe to change: `make journeys` always starts from `down -v`, so the account is
created fresh each run, and `auth.py` makes the first user it sees the admin
regardless of `sub`. Nothing else in the tree referenced it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fix(e2e): J6 stops at the session's door, and scans a settled page (#726)
All checks were successful
CI / Bot/backend version sync (pull_request) Successful in 25s
CI / Backend lint (ruff) (pull_request) Successful in 1m3s
CI / Feedback site tests (pull_request) Successful in 1m21s
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m41s
CI / Bot tests and audit (pull_request) Successful in 2m44s
CI / Frontend tests, audit, and build (pull_request) Successful in 3m24s
CI / Docker image build (pull_request) Successful in 3m30s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Successful in 4m0s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 11m53s
CI / Backend migration, tests, and audit (pull_request) Successful in 14m47s
0d30b7c96c
CI was right and the local runs were not. The session page
(`SessionDetail.jsx`) fails `color-contrast` on **thirteen** nodes, and
J6 had been certifying it clean twelve runs in a row.

Not a race in the seed, and not the shell putting a dark chip on light
paper: the chips are bare `bg-amber-900/50 text-amber-300` and
`bg-green-900/50 text-green-300` with **no `dark:` prefix**, so they are
dark-intent styling applied unconditionally and have been wrong in light
mode since long before the redesign — axe prints `dark:` classes in its
selectors when they are there, and for these it does not. Three scans of
the settled page report the same thirteen nodes, so the page itself is
deterministic.

What was not deterministic was **what J6's scan was pointed at**. The
two assertions before it — the session's title, and the recap text —
are both satisfied by the session request alone, while every failing
element belongs to a second wave (the roster's RSVP chips, and the
post-session checklist, which renders at all only because this seed
writes post-session task rows). A readout at the scan moment shows that
second wave absent; axe reports it present, because it arrives inside
axe's own injection window. Whether the scan sees it is therefore
decided by how fast the machine is: green here, red on the runner.

So:

- **J6 stops at the door.** It asserts that "Read the full recap" and
  the aside's "Read the recap" name the same session, and goes no
  further. `/sessions/:id` is lane 4's to rebuild and J5's to walk —
  "after the session: the checklist, transcript and summary review" —
  and lane 3's acceptance gate should not be held on re-colouring a page
  lane 4 deletes. The thirteen nodes are measured and listed in the PR
  rather than tolerated; `a11y-baseline.js` stays `{}`.
- **Every scan now waits for its page's *last* request**, not its first.
  This week waits for the campaign card at its foot; the wiki front
  waits for the story and the "Last updated" line — which is the element
  this branch fixed, so that scan was a coin-flip in the other
  direction. The rule is written into the harness README beside the
  scan rule it qualifies, because a scan whose target is a race
  certifies nothing and fails where it is least likely to be believed.

Verified with `--repeat-each=3`: nine of nine, run times clustered
within a second where they used to vary.

Also folds in `ruff format` on the seed (the backend lint job's pin is
0.4.4; `ruff check` and `ruff format --check` both clean over
`webapp/frontend/e2e/`).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Author
Collaborator

The red CI run: CI was right, the local runs were not

Pushed as 0d30b7c. A correction to this PR's description first, because it named the wrong cause and I would rather fix it at the top than quietly edit it:

the session page … is axe-clean today, so scanning it costs nothing

That was wrong on both halves, and it is the claim that cost the red run. The session page fails on 13 nodes, deterministically — reproduced three times, identical selectors and ratios each time, matching CI exactly. And scanning a page built from pre-redesign markup costs whatever that page's debt is, which is a reason to know the debt before adding the step, not a reason to add it and find out.

I also guessed above that the blended backgrounds might be dark-variant classes leaking into light mode, which would have made this a shell-integration fault and lane 3's. That is wrong too. The chips are bare and unconditional:

running: "bg-amber-900/50 text-amber-300"    yes: "bg-green-900/50 text-green-300"

No dark: prefix and no light counterpart — confirmed twice over: axe prints dark: classes in its selectors when they exist (it does for a different element on the same page) and prints none for these, and the computed style is a translucent dark green actually applied in light mode. The old app has supported light mode all along, so these have been wrong since before the shell existed. Filed as #775, and named on lane 4's issue.

The actual bug was in the journey, and it is the interesting part

The page fails every time. What was intermittent was what the scan was pointed at.

The two assertions before axe() were the session's title and the recap — and both are satisfied by the session request alone. Every failing element belongs to a second wave: the roster's RSVP chips and the post-session checklist. A readout at the exact scan moment:

PROBE[at-J6-scan-moment]  {"hasChecklist":false,"hasRsvpChips":false}  violationNodes=13
PROBE[one-scan-later]     {"hasChecklist":true, "hasRsvpChips":true }  violationNodes=13

The second wave is absent when the assertions clear and present by the time axe finishes — it arrives inside axe's own injection window. So whether the scan saw it was decided by machine speed. Twelve green local runs (four full suites × three projects); the runner lost the coin-flip. That is the worst failure mode a gate can have, and it was mine, not CI's.

Worth saying how the wrong claim got made: from a throwaway probe that did page.goto() then axe() with no wait at all, scanning a page that had barely begun rendering — and then twelve green runs "confirmed" it.

Fixed in three places, not one

  1. J6 stops at the session's door. It asserts that "Read the full recap" and the aside's "Read the recap" name the same session, and goes no further. /sessions/:id was never in J6's scope — the contract's recap act is This week's SE-006 — the markup is lane 4's to delete, and lane 3's acceptance gate should not be blocked on lane 4's debt. The spec carries the diagnosis in place so lane 4 inherits it rather than rediscovering it.
  2. Every scan now waits for its page's last request, not its first. This week waits for "About the campaign" (a second request) before scanning. The wiki front waits for the story prose and the Last updated line — and that one matters: Last updated is the element this branch fixed, and the searchbox the scan had been waiting on is static first-paint markup, so that scan was a coin-flip in the other direction and the violation it found was luck. Deterministic now.
  3. The rule is in the harness README, beside the axe() rule it qualifies, with this failure as the worked example: wait for the slowest thing on the page, and pick it deliberately — a scan whose target is a race certifies nothing, and it fails in the place least likely to be believed.

a11y-baseline.js is untouched and still {}.

Verification

  • Whole suite 24 passed, 6 skipped.
  • J6 --repeat-each=3: 9 of 9, run times now clustered within a second (9.6–10.6s) where they used to vary — consistent with deterministic waits rather than a race.
  • eslint e2e src/pages/WikiFront.jsx: 0 errors. ruff check and ruff format --check clean over e2e/ with the pinned 0.4.4.
  • Also run against current main (four commits ahead, including #769's Settings, which touches ui/Doc/index.jsx that J6's sectionOf() helper reads) on a scratch branch: 24 passed. The branch is behind but will need no spec changes to merge.

🤖 Generated with Claude Code

## The red CI run: CI was right, the local runs were not Pushed as `0d30b7c`. **A correction to this PR's description first**, because it named the wrong cause and I would rather fix it at the top than quietly edit it: > the session page … **is axe-clean today, so scanning it costs nothing** That was wrong on both halves, and it is the claim that cost the red run. The session page fails on **13 nodes**, deterministically — reproduced three times, identical selectors and ratios each time, matching CI exactly. And scanning a page built from pre-redesign markup costs whatever that page's debt is, which is a reason to *know* the debt before adding the step, not a reason to add it and find out. I also guessed above that the blended backgrounds might be dark-variant classes leaking into light mode, which would have made this a shell-integration fault and lane 3's. **That is wrong too.** The chips are bare and unconditional: ```js running: "bg-amber-900/50 text-amber-300" yes: "bg-green-900/50 text-green-300" ``` No `dark:` prefix and no light counterpart — confirmed twice over: axe prints `dark:` classes in its selectors when they exist (it does for a different element on the same page) and prints none for these, and the computed style is a translucent dark green actually applied in light mode. The old app has supported light mode all along, so these have been wrong since before the shell existed. Filed as **#775**, and named on lane 4's issue. ## The actual bug was in the journey, and it is the interesting part The page fails every time. **What was intermittent was what the scan was pointed at.** The two assertions before `axe()` were the session's title and the recap — and both are satisfied by the *session* request alone. Every failing element belongs to a **second wave**: the roster's RSVP chips and the post-session checklist. A readout at the exact scan moment: ``` PROBE[at-J6-scan-moment] {"hasChecklist":false,"hasRsvpChips":false} violationNodes=13 PROBE[one-scan-later] {"hasChecklist":true, "hasRsvpChips":true } violationNodes=13 ``` The second wave is absent when the assertions clear and present by the time axe finishes — it arrives **inside axe's own injection window**. So whether the scan saw it was decided by machine speed. Twelve green local runs (four full suites × three projects); the runner lost the coin-flip. That is the worst failure mode a gate can have, and it was mine, not CI's. Worth saying how the wrong claim got made: from a throwaway probe that did `page.goto()` then `axe()` with **no wait at all**, scanning a page that had barely begun rendering — and then twelve green runs "confirmed" it. ### Fixed in three places, not one 1. **J6 stops at the session's door.** It asserts that "Read the full recap" and the aside's "Read the recap" name the same session, and goes no further. `/sessions/:id` was never in J6's scope — the contract's recap act is This week's `SE-006` — the markup is lane 4's to delete, and lane 3's acceptance gate should not be blocked on lane 4's debt. The spec carries the diagnosis in place so lane 4 inherits it rather than rediscovering it. 2. **Every scan now waits for its page's *last* request, not its first.** This week waits for "About the campaign" (a second request) before scanning. The wiki front waits for the story prose **and** the `Last updated` line — and that one matters: `Last updated` is the element this branch fixed, and the searchbox the scan had been waiting on is static first-paint markup, **so that scan was a coin-flip in the other direction and the violation it found was luck**. Deterministic now. 3. **The rule is in the harness README**, beside the `axe()` rule it qualifies, with this failure as the worked example: *wait for the slowest thing on the page, and pick it deliberately — a scan whose target is a race certifies nothing, and it fails in the place least likely to be believed.* `a11y-baseline.js` is untouched and still `{}`. ## Verification - Whole suite **24 passed, 6 skipped**. - J6 `--repeat-each=3`: **9 of 9**, run times now clustered within a second (9.6–10.6s) where they used to vary — consistent with deterministic waits rather than a race. - `eslint e2e src/pages/WikiFront.jsx`: 0 errors. `ruff check` and `ruff format --check` clean over `e2e/` with the pinned 0.4.4. - Also run against current `main` (four commits ahead, including #769's Settings, which touches `ui/Doc/index.jsx` that J6's `sectionOf()` helper reads) on a scratch branch: **24 passed**. The branch is behind but will need no spec changes to merge. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Merge main into build/lane3-j6 (#726, #729)
All checks were successful
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m33s
CI / Backend lint (ruff) (pull_request) Successful in 44s
CI / Bot/backend version sync (pull_request) Successful in 34s
CI / Feedback site tests (pull_request) Successful in 1m6s
CI / Docker image build (pull_request) Successful in 45s
CI / Bot tests and audit (pull_request) Successful in 1m59s
CI / Frontend tests, audit, and build (pull_request) Successful in 4m55s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Successful in 4m44s
CI / Backend migration, tests, and audit (pull_request) Successful in 16m29s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 19m45s
252c54eae0
#774 (the session form and the series) landed underneath. One conflict, in
`CHANGELOG.md`; both entries kept.

No code conflict, but #774 touched `ThisWeek.jsx`, `Sessions.jsx` and
`useContents.js` — This week is J6's first act and the Contents is how
`goVia` reaches Highlights and Storyline, so the journeys job is the check
that matters here rather than the unit suite.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign in to join this conversation.
No description provided.