adventure: work the five review findings the last pass left open

The extract pre-check is gone. It read a snapshot up to two minutes behind and
still got the last word, so somebody who set out over Matrix during a lagging
roster push was told they weren't on an expedition for a run gogobee would
happily have ended. Same call abandon and leave already made: let it through and
let rejected_not_running be the answer.

The siege_join check stays, because whether a boss is camped outside town is
town-wide and runs on a day-or-longer clock, but it now reads one column through
SiegeIsCamped instead of loading every defender row and the whole history to
look at one flag.

The war-room history insert is OR REPLACE. boss_id is the primary key and it was
never settled whether gogobee means the siege instance or the boss type by it, so
a duplicate pair used to fail the transaction carrying the live boss and the
muster too and freeze the war room on the last good snapshot. A dropped history
row is the smaller failure; the open question is noted in the schema.

offersToUndo's guard didn't cover the case its comment claimed. A gogobee too old
to push seats sends a valid blob with no party key, which decodes to the same
empty slice as a solo run, and a party member got shown the button that throws
away everyone's day. That needs a new field, so whoDetail gains party_known and
the flag gates the empty-list branch alone; the branch that reads the viewer's
own seat is self-evidencing and keeps working against any sender. gogobee's half
is written up in adventure_party_known_flag.md.

And an empty offer list no longer claims "you're already out there", which Pete
can't actually know from a game box too old to push offers at all.
This commit is contained in:
prosolis
2026-07-24 22:45:26 -07:00
parent c40ac1e673
commit aac6c3e127
9 changed files with 232 additions and 232 deletions
+75
View File
@@ -0,0 +1,75 @@
# Wire contract — `party_known` on the public detail blob (gogobee side)
Status: **Pete side BUILT + TESTED. gogobee side NOT BUILT.** Written 2026-07-24 out
of a code review of the W9 verbs branch. Small: one boolean, set unconditionally,
no new endpoint and no new verdict. Companion to `adventure_expansion_spec.md`.
## The hole
`offersToUndo` in `internal/web/who.go` decides which of "Call the whole thing off"
(abandon) and "Leave the party" (leave) an owner's page offers. On a live expedition
with an EMPTY party list it took the "solo run ⇒ this player is the leader" branch,
because a solo run publishes no seats at all.
But a gogobee too old to push party seats sends a perfectly valid detail blob with no
`party` key, and that decodes to the same empty slice. So a party MEMBER served by an
old game box was shown the leader's button — the one that throws away four people's
day. gogobee refuses it with `rejected_not_leader`, so the live cost was a misleading
offer rather than a lost expedition, but the page was asserting something it could
not know.
Nil and empty cannot be told apart on the current wire, hence a new field.
## What gogobee must send
The public detail blob on the roster push (the one Pete decodes into `whoDetail`)
gains:
```json
"party_known": true
```
**Set it unconditionally**, on every sheet, from any gogobee build that knows what a
party seat is:
- on a solo run — YES, this is the case the flag exists for;
- when the party list is omitted because there is nobody else — YES;
- when the player is standing in town — YES;
- when the sheet does carry seats — YES, harmless there.
It is a capability flag about the **sender**, not a fact about the character. It must
never be computed from whether there is a party, whether the run is shared, or
anything else on the sheet. A conditional `party_known` is worse than none: it would
read as "this player is solo" and put the flag straight back into the hole it was
added to close.
Anything that omits it (old builds, JSON that fails to decode) lands as `false` in
Go, which is the fail-closed answer Pete wants.
## What Pete already does with it
- `whoDetail.PartyKnown bool \`json:"party_known"\`` — `internal/web/who.go:47`.
- `offersToUndo`'s third parameter is `partyKnown`; the call site passes
`page.HasDetail && page.Detail.PartyKnown`, so a blob that failed to decode is
still excluded.
- The flag gates the `len(party) == 0` branch **only**. The `len(party) > 0` branch
reads the viewer's own seat and is self-evidencing — a seat that says "member"
cannot be mistaken for leadership — so it keeps working against every sender,
including ones that never set the flag.
- Cases pinned in `internal/web/orders_undo_test.go`.
## Rollout
Pete is already deployed-safe: it ships first and costs nothing but one button. Until
gogobee sets the flag, a genuine SOLO leader on a live run does not see the abandon
button on the party branch. They are not stuck — the `self.Resume != nil` clause
still offers it for an extracted run from town, and abandoning over Matrix was never
affected. The moment gogobee ships the flag the button comes back with no Pete change.
## Done when
- gogobee sets `party_known: true` on every public detail sheet it builds.
- A solo adventurer on a live expedition sees "Call the whole thing off" on their web
page again.
- A party member on the same build still sees "Leave the party" and never the abandon.
- Delete this file.