Issue #3618603: Pre-alpha3 audit: give the calendar day grid an accessible name and state, throttle every path that places a hold, and refresh the figures, vocabulary and pages the last seventy commits moved
Audits [#3618603].
What is here, in the order the issue lists it.
1. The calendar day grid. Each day is named in full and its state is a word:
aria-label carries the weekday, the date and whether it can be booked, because
the number in the cell was everything a sighted visitor needed and nothing a
reader could use. aria-pressed says whether a day is chosen. A day that cannot
be booked carries aria-disabled instead of disabled, for the reason
stepper.js and venue-map.js both give in their own comments, and pressing it
announces which day it is and what is wrong with it. The validity note becomes
the one live region (role="status", built once and rewritten, never replaced),
the submit is marked rather than switched off and describes itself through that
region, the month arrows get names, the weekday initials are hidden from the
reader that does not need them, and the stylesheet gets a focus ring.
BookingCalendarTest gains a scenario for the naming and the region, and its
existing assertions move from disabled to aria-disabled. Run in a browser:
53 assertions, green.
2. One throttle for every path that places a hold, and it ships OFF.
HoldThrottle in the engine module, asked by the booking form, the offer card's
claim and the place map's click, on one flood event with one setting. It
deliberately does not live in BookingManager: the engine holds under cron and
in a queue worker, where there is no client to count. The setting moves from
yoyaku_calendar.settings to yoyaku.settings as hold_flood.limit and
hold_flood.window, because the place map must not depend on the calendar to be
capped; the calendar's settings screen had nothing else on it, so the form, its
route and its menu link go, and the two fields appear on the site booking
settings screen beside the rest. The endpoints answer 429 with a sentence a
booker can read. Giving places back is never throttled: PlaceSelectionController
asks only where the request names something to take, since refusing a release
would leave a booker holding places they asked to be rid of.
The cap is nought as shipped, and that is the considered position rather than a retreat. A check here cannot stop a flood: by the time it answers, the site has bootstrapped, built its container, resolved the route and decided access, so what a refusal saves is the lock and the write at the end. Anything big enough to hurt has to be turned away upstream. So it is a policy about one misbehaving client rather than a defence of the machine, and it counts per ADDRESS, where a wrong guess lands on the wrong person: a school or a town hall is a crowd of legitimate bookers sharing a count, meeting the cap during exactly the rush when refusing costs most. An operator who has seen a client draining capacity turns it on, and the setting says so where they read it. Nothing about how much anybody may book depends on it.
That also retired the ten a minute the form had carried since it was written, which the browser tests showed to be a number a PERSON reaches: a form submits once per booking, but the offer card and the seat map post once per run of clicks, so picking a party seat by seat is dozens of attempts a minute. Ten was never generous, it was untested.
ClaimThrottleTest is new and was seen to fail against the unfixed endpoint
("Failed asserting that 200 is identical to 429") before it passed against this
branch; it now also asserts that a site on the shipped configuration is not
throttled at all. PlaceBookingTest gains the take-is-capped, giving-back-is-not
pair. Ten classes that drive a booking surface declare, through a trait, that
they are not about the cap, including the two rigs that fire many requests from
one address on purpose: a seat-picking test should not be able to fail on a rate
limit.
3. The cost register. A second dated column in PERFORMANCE.md, measured on
this head with the cost tests' own instrument, keeping 2026-08-12 beside it: a
hold costs one statement more and seating three, both of them the anchor counters
[#3617101] introduced, while the reads of the place table fall from five to one
and the orphan rule costs five fewer. The hold figures were re-run on MySQL and
came back identical, statement for statement, so the column is not the driver.
The sentence explaining the read that does not grow is corrected: what the test
counts is the counter, not the grouped sum it replaced.
4. The pages that went stale. docs/performance.md says which head its
figures predate and which four issues moved them, since it is measured from
before the locking work and an operator sizes a rush from it. docs/metrics.md
is regenerated (1,116 files to 1,262) and now says to regenerate it as a release
step, which is what it has needed for three cycles. Seven "used to" passages go.
The README gains the yoyaku_presentation row it was missing and describes the
lock as it is taken now.
5. Vocabulary. Around fifty money words in the tests and the load harness
that [#3618369] did not reach, including a class named SeatSoldOnceTest, a
scenario naming a "sold out session", and the rig's printed column header; plus
oversell in shipped engine code. And some nine hundred comment lines that said
session where they meant the slot, which is the one word this codebase cannot
spare: it is the visitor's own session everywhere else. Swept by hand where the
meaning was the visitor's, and the French carries onto every reworded string
rather than being written again.
6. Tooling. _LENIENT_ALLOW_LIST names the development dependencies that do
not declare Drupal 12 yet. This is a prerequisite and not the whole answer, and
the lane still does not build. Played on this branch, the job log shows the list
applied ("Using lenient allow list: drupal/orchestra, ...") and composer still
gives up on drupal/orchestra[dev-1.x] require drupal/core ^11.3: the lenient
plugin did not relax that constraint. So the lane unblocks when orchestra itself
declares the major, which is being done separately, and this change is what stops
the list being the next thing to discover. Recorded rather than claimed, because
an empty bot diff and a lane that cannot build look identical from outside. The phpstan gate moves from the templates' level 0 to level 1, which this
module already meets with nothing reported. A breadcrumb builder declares the
CacheableMetadata argument its interface requires in the next major, which
removes a deprecation from every page it is asked about.
7. Smaller. BookingSlot's rule base field, its getter and its interface
method go: provisioned for yoyaku_recur, which is unbuilt, and nothing wrote or
read it. ModuleBoundariesTest gets the process isolation every other class has.
Three files stop ending with two newlines.
Checked before pushing. phpcs over the module root under
Drupal,DrupalPractice: no error, and no warning outside the Markdown long lines
CI does not scan. phpstan level 1: nothing. prettier and stylelint on the touched
JavaScript and CSS: clean. eslint: no error. cspell over the changed files: one
coined word of mine, reworded rather than added to the dictionary.
scripts/check-translations.php: every shipped string translated, and a literal
search back the other way finds no dead entry. ClaimThrottleTest,
PlaceBookingTest, CalendarBookingFormTest, BookingCalendarTest,
ModuleBoundariesTest and the nine classes the vocabulary pass touches: green,
the first three on both drivers.
Not done here, and why. The load rig has not been re-run on this head: that
is a machine exercise rather than a code change, and the page says so rather than
implying otherwise. Raising phpstan past level 1 is its own issue: level 2
reports 264 and level 3 reports 356, but 219 of those are one pattern, a call on
a variable typed as EntityInterface, and a drupal.entityMapping block takes
level 2 to 158 on its own.
Second commit, after the first pipeline. CI found two things, and one was
worth more than the test it broke: the default cap above (a number a person
reaches), and a fieldset that does not nest what it groups unless told to, so
the two new settings fields posted flat while the submit handler read them
nested, wrote nought for the window, and nought is a window the schema refuses.
The whole booking settings form stopped saving, and BookingSettingsFormTest
caught it. Named flat now, as the rest of that screen already is. Re-verified
locally: BookingSettingsFormTest 34 assertions, OfferStepperTest 82,
VenueMapPickerTest and PlaceMapPerformanceTest 321 together, the three
throttle-touching kernel classes on both drivers.