The engine's structure held up under a night of adversarial grepping: the enforced import boundary was never broken, one writer constructs every booking, four of six seams are already domain-neutral, and BookingUnitInterface already says "Places, Nights, Units". What had drifted was the dictionary, and the words are what keep re-introducing the leaks: the comment directly above one of the reach-ins fixed in #3618663: Take the engine out of another module's schema, in the two places it reached in talked about "a picker that had the hall open".

Two rules govern this work, both learned the hard way in the session that produced it. Do not derive the architecture from the code, because that only ratifies the code. And do not invent vocabulary: this problem has a literature.

The vocabulary is revenue management's

Capacity is sold into classes. A booking limit caps a class. A protection level holds capacity out of a class for a named holder, optionally releasing at a cutoff. A sub-inventory is a pool physically separate from another. A rate plan prices a class. Which specific unit a booking receives is assignment, which hotel systems keep deliberately separate from revenue management: how many is one question, which room is another.

Mapped onto what exists: a slot's capacity is capacity; a section or area is a sub-inventory; a tariff crossed with a grade is a class; a tariff quota is a booking limit; an allotment is a protection level with a release; and the placement provider is assignment.

The engine was already right

This matters more than the renaming. min(quota_left, capacity - consumed - withheld) shares capacity rather than carving it, so an unsold seat in one class stays sellable in another. That is nested control, which is what airline revenue management converged on, and the literature is blunt about the alternative: with distinct allocations a full class refuses a booker while a cheaper class sits open beside it. A booking limit and a protection level are two ways of writing the same nested control down, which is why an engine wants both and it is not duplication.

So this programme expresses once what the engine already gets right four times over. It is not a correction.

Proven, in #3618712: Write the availability sum as the inventory controls it already is: the availability sum is one fold over controls, checked against the old six-argument signature across 2400 combinations including absurd inputs, with every cost figure unchanged.

What is left, each provable or killable on its own

The vocabulary. Two seams renamed: the area provider becomes the sub-inventory seam, and the placement provider becomes assignment. Ten flag string literals get homes as arguments or properties. Around 250 prose sites in the engine stop being written in theatre: seat 171, hall 39, "the house" 32, stalls 5, balcony 2. By hand, because a scripted rewrap eats a closing comment delimiter and kills a file silently. The engine speaks the neutral words; the placement module keeps seats and halls, which are correct for it.

The values bag. One contract returning the columns a module records on a row, replacing four magic keys read in eleven places, every one of them the placement module talking to itself through the engine. Prove it on three call sites, with the cost log identical, before converting the other seventy-eight.

The four controls as one. Whether booking limits, protection levels, sub-inventory counts and class quotas collapse into one configured control rather than four hardcoded entity families. This is the large one and it needs its own spike first.

The stored value of the partition scope. #3618720: Call a partition of a resource a partition, in the engine and across the seam renamed SCOPE_SECTION to SCOPE_PARTITION and deliberately left the value it stores as section: it is a key in the anchor table, nothing reads it as a word, and what is asked of it is that it stay the same so the rows a running site waits on go on being those rows. Moving it is its own pass. Every existing row scoped the old way becomes unread, and while the table's repair path handles that (it logs the missing anchor, writes it in a fixed order so two repairs cannot cycle, and recounts because the counter starts at nothing), doing it lazily means one warning per partition per slot on first hold and a gap-lock repair under a burst. Write the anchors in advance instead, with a pass calling ensure() over existing slots before the new code serves traffic, or reinstall, which is the declared upgrade path before 1.0. The boundary guard's one remaining exception exists for this value and is deleted with it.

The name lists. Four places where the engine hardcodes another module's field names: an entity title scope list, a field-group mapping, and a defaults plus followed-fields pair in the booking hooks, one of which defaults a field to a plugin id the engine cannot provide.

Not doing

A separate module for identity. Identity is one optional axis and the engine already has the seam with an anonymous default that most resources on the maintainer's own site use; a module would have delivered optionality nobody asked for. And a generic booking request type: it grew to ten fields of which two are only ever written by the engine, and used nullability as a discriminator, which is the implicit shape it claimed to replace. Both real defects found here were fixed by moving a rule into the engine, not by typing a payload.

Standard for every phase

Full suite on both CI lanes with the next-major composer job played, since a skipped lane is not a green one. Cost logs diffed figure by figure. The boundary guard extended and seen to fail before it passes. Seating classes first for anything touching flags. And consumers traced by service wiring rather than interface name: the one regression in this work came from a service bound to a concrete class, which no search for the interface could find.

What shipped

Four children and one final change, all merged. #3618720: Call a partition of a resource a partition, in the engine and across the seam renamed the engine's word for a subdivision of a resource from area to partition, and took three published shapes out of another module's schema. #3618731: Carry the fields a module puts on a booking in a type of their own gave a line's extra field values a type of their own, so the engine stops carrying an unnamed bag of another module's field names. #3618762: Let each entity type say what its rows belong to, instead of the engine holding the list let each entity type say what its rows belong to, deleting two lists of field names the engine held. #3618769: Let each field say how a rule reaches it, instead of the engine holding the lists let each field say how a rule reaches it, deleting three more.

The last change took every submodule out of the engine's words. Thirty-one references by name, of which twenty-eight were prose, plus two hundred and twenty-five occurrences of seat, seated, seating, hall, house, balcony, stalls and theatre, which now say unit, placed, placement, resource and part. Two theatre words survive on purpose and both name three domains rather than one, which is how the prose says it is about none of them.

Three were not rewordings. The translation pass ordered itself after three submodules by name and now orders itself last. The breadcrumb linked to an administration route and now asks a seam, so a site without that UI has no step there. And a form id began with the name of the module that has orders, which it never meant: it sets weights, so it says weight.

Each replacement ships with a guard that was seen to fail before it passed, and the last one reads the submodule list off disk, so a submodule added tomorrow is covered without anybody remembering.

Still open, and deliberately not folded in: the stored value behind the partition scope constant, which needs its anchors written in advance or a reinstall; and the four controls a resource carries, which want a design of their own rather than a rename.

AI-Generated: Yes (Claude Code produced the measurements, the research and this plan. Five earlier versions were abandoned, four of them refuted by a single search each, and the vocabulary is taken from revenue management practice after the maintainer twice corrected the method.)

Issue fork yoyaku-3618694

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

mably created an issue. See original summary.

mably’s picture

Title: Separate identity from capacity: a yoyaku_unit layer, and a dictionary that is not theatre » Express the capacity model once, in revenue management vocabulary
Issue summary: View changes
mably’s picture

Issue summary: View changes

mably’s picture

Status: Active » Needs review

  • mably committed c0e7ede2 on 1.x
    fix: #3618694 Express the capacity model once, in revenue management...
mably’s picture

Status: Needs review » Fixed

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

mably’s picture

Title: Express the capacity model once, in revenue management vocabulary » Take every submodule out of the engine's words, in code and in prose
Issue summary: View changes

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.