Compare commits

2 Commits
Author SHA1 Message Date
prosolis 5b07199631 Merge party-known-flag: retire the party_known write-up, gogobee sets it now 2026-07-24 23:16:30 -07:00
prosolis a14859c5ee adventure: retire the party_known write-up, gogobee sets it now
The flag was built and tested on this side first and the contract was left
in the tree for the game box to pick up. It has (gogobee 10d1e4a, set
unconditionally on every detail sheet), and a solo leader on a live
expedition has their abandon button back, so the note has nothing left to
ask for.
2026-07-24 23:04:05 -07:00
-75
View File
@@ -1,75 +0,0 @@
# 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.