Problem/Motivation

A party bigger than any single run in the house is left split across two sections when one section could have held all of it.

The shape of it: a party holds places at one grade in a section, having grown a few units at a time. It grows once more, that section has no room left at that grade, and the newcomers are seated in a second section. Another section is holding far more free places at that grade than the whole party needs, in runs long enough to seat it in two pieces. Nothing ever puts the party there, then or afterwards.

The engine had read the whole house before it answered. The paging loop in VenuePlacementProvider::place() keeps turning pages while the attempt is not settled, and settled means SeatingCloseness::Adjacent. Where no row anywhere holds the party side by side it never settles, so every section is read and evaluated. The information was there. Nothing asked the question.

What it asks instead is in Together::propose(). Extending around the party is tried first and preferred. When that fails, replace() is the one rung that hands back what the party holds, and it looks for a single usable run of the whole party's length, row by row, returning Adjacent when it finds one. Where the party is longer than the longest run in the house, it returns NULL on every row of every section. Every rung after it keeps the held places pinned as anchors and seats only the units still to place, which is how the newcomers end up in a section of their own.

The edge is sharp enough to see from the outside. A party that exactly fills an unbroken run somewhere in the house is released and re-placed into it, crossing sections to get there, and lands side by side with nothing to report. The same party one unit larger no longer fits that run, no other row is long enough, and it stays where it is with the extra unit seated in a section of its own. One unit is the whole difference between the best arrangement in the building and one of the worst, because the only alternative to a perfect move is not moving anybody.

So the shape of the gap is: the engine can move a whole party only when the move is perfect. There is nothing between "all of them side by side in one row" and "leave the ones who are seated and put the newcomers wherever fits".

Proposed resolution

Give the whole-party move a second bar. replace() answers "is there one run for all of them". Beside it, ask "is there an arrangement of all of them that is strictly better than the one they have", and move only when the answer is yes.

Both halves of the score already exist. The closeness rank is SeatingCloseness::getRank(), and the run count is what rung 6, Together::getFewestRuns(), already minimizes across rows, areas and sections. Today both are applied to the units being placed with the held places pinned; this asks them of the whole party, the seated included.

On the case above: what the party holds is three runs across two sections, which the ladder calls scattered. The candidate is two runs in one section. Strictly better on both counts, so the party moves.

Strictly better or no move, and this is the whole of the safety. A section with room to spare but only single places scores worse on runs than the three the party already sits in, so it loses; a test on free capacity alone would take that move and make the booking worse. A tie keeps what the party has, because a move is not free: somebody has told a friend where they are sitting, which is why "Your places are in another row now." exists as a sentence.

It reuses getStretches() and the even-split search rung 6 already carries, runs only where replace() has returned NULL, and reads nothing new: the candidates are in memory precisely because the attempt never settled. hasChosenAnchor() keeps its meaning untouched, so a party that picked any of its own places is never moved.

What decides the move, and why it is not pieces

Pieces stay the measure of how good an arrangement is. The check that a move is no worse reads the pair of pieces and closeness in that order, which is rung six's order and not a new one. Nothing about how the engine judges an arrangement changes here.

What needed deciding is something else: whether to disturb people who are already sitting down. A move is not free. It hands seats back, takes others, and tells a booker their places have changed. So the rung needs a threshold and not merely a direction, and the threshold chosen is that the party ends up in fewer parts of the house.

Written first as simply "better", it fired on every marginal gain. A party grows one press at a time and an ask that does not fit beside it is ordinary rather than rare, so bookers were carried around the building press after press, told each time that their places had changed, to arrive somewhere no better than staying would have taken them.

The cost of the threshold, said out loud because the diff cannot say it: a move that would put a party in fewer pieces without putting it in fewer parts is not made here. Four pieces down to two, all inside one section, is a real improvement and this rung declines it. That case belongs to the consolidation pass, which moves a piece in beside another within the reach it already has; where it falls through is where that pass cannot reach, since it reads only the sections the party occupies.

And the honest weakness. Parts is a proxy. What the rung wants is a price for disruption: moving people who are seated should have to buy more than a marginal gain, and how much more should scale with how many of them there are. Parts correlates with that well enough to work, which is not the same as parts mattering more than pieces. Anybody revisiting the threshold should replace the proxy rather than tune it.

Remaining tasks

The rung and the comparison; kernel coverage for a party that should move, one that must not because the alternative is single places, one holding a chosen place, and one where the arrangements tie; whatever docs/seating.md says about the ladder.

User interface changes

None of its own. A party moved this way is already covered by "Your places are in another row now.", and what it is told about how it ended up sitting is the closeness sentence it would have been told anyway.

Also here: the seating says what it does

Nine methods of the strategy return a proposal, which is to say they are attempts that either seat the party or answer nothing, and only two of them said so. getSpread() ran three tiers of search and then decided whether the whole proposal was a last resort, which is a policy judgement wearing an accessor's name; getGrowFromTheParty() recursed into the entire rest of the ladder, which made it one of the most expensive calls in the class and one of the cheapest-looking. Every rung is a verb now, so the ladder reads as the ordered list of attempts it is, and the new rung above is written in that vocabulary rather than adding to the old one.

And one idea carried four words. A run, a stretch, a fragment and a piece all meant seats next to each other, and three classes each grew their own getStretches(). The code had already settled this and lost track: getFragments() documented its own return as "the runs". There is one noun now, and the distinctions that are real are carried by the method names: getRuns() answers windows of a given length, getWholeRuns() the maximal ones, getPartyRuns() the party's own.

Also here: the sweep meets a house with people in it

The sweep that arrived with the seating fixes ran against empty theatres, which is the one thing a real house never is, and exactly the shape that hides this class of fault: every rung the engine prefers looks for an unbroken run, and in an empty hall it finds one every time. The rungs that answer when it cannot are where every fault of the last two issues lived.

Other bookers now hold seats before the party arrives, in pairs so that the seeding itself does not strand a place and get refused by the rule under test. It also catches coming up short rather than only being refused, since a booker who asked for seats the house had and did not get them has one complaint whichever exception the engine raises. 823 bookings, none skipped, none failing.

API changes

The new rung is private to Together and SeatingStrategyInterface is unchanged. The renames reach two public methods of PlaceGeometry: getStretches() and getFragments() become getWholeRuns() and getPartyRuns(), alongside getApart(), getIsolation() and run(). All are called only inside this module, and this is pre-1.0.

Data model changes

None.

Related

The same condition appears a third time, and this one is the plainest to see. Together::consolidate() takes the party's length and looks for one unbroken run of it in a row, so for a party longer than the longest row it returns without trying anything. A party occupying three full rows and holding two places in a fourth, with two free places left in the third by its own last take, is not tidied by moving those two up into them: that is not the whole party in one run, so it is not considered. Nothing else will do it either, and the arrangement stands for the life of the order.

So every whole-party manoeuvre in the strategy is all-or-nothing on a single unbroken run: replace() at placement, the may-strand escape that goes through it, and consolidate() afterwards. A party larger than the longest row in the house is helped by none of them, which is one condition to change rather than three features to add. The score this issue proposes is what replaces it, and it applies to the later pass as readily as to the first.

VenuePlacementProvider::consolidate() narrows it further still: it reads only the sections the party already occupies, so even with the condition fixed it could close a hole inside one section and never bring two sections together. That part is worth its own look once this one is settled.

AI-Generated: Yes (Claude Code diagnosed this from a reproduction and the code, proposed this design and drafted this summary. I reviewed it before posting.)

Issue fork yoyaku-3620010

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

Issue summary: View changes
mably’s picture

Issue summary: View changes
mably’s picture

Status: Active » 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

Status: Fixed » Active

mably’s picture

Status: Active » Needs review
mably’s picture

Issue summary: View changes
mably’s picture

Title: Weigh a whole-party move when no row can seat them all, so a party is not left split across sections » Weigh a whole-party move when no row can seat them all, and name the seating for what it does
Issue summary: View changes

  • mably committed aae2401d on 1.x
    fix: #3620010 Weigh a whole-party move when no row can seat them all, so...
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.

Status: Fixed » Closed (fixed)

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