Reported three times across three sessions, two days apart, and it
survived a whole UI rebuild: "i cant see whos support i accept".
It was not styling. The move button's label was format!("{c:?}"), so the
player read `RespondToSupport { response: AcceptBond }` — Rust struct
syntax with no name in it. And the command does not carry the
counterparty, so nothing rendering it alone could have said who; it comes
off the view, as whoever played Support at this seat.
Four more seat-panel fields had the same defect, including `support
AcceptBond` — the one the report names. CB-WP-0020 fixed exactly this for
selections and left its four neighbours as they were.
command_label has no catch-all arm, and that earned its keep before any
test ran: GroundChoice::RejectReverse and SupportResponse::BreakRivalry
both failed to compile — two moves that would have shipped as struct
dumps. An offer the view cannot see is said to be unseen rather than given
an invented name.
The finding underneath: the coverage probe that exists to prove every view
field reaches the PLAYER was matching "player: Some(PlayerId(1))" and
"members: [PlayerId(1)". It was certifying the defect as coverage and
would have gone red had anyone fixed it. Second confirmation of
CB-WP-0024's finding, from the sharper side: a probe naming Debug output
does not merely fail to protect, it pins the defect in place.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
edab11c0e4
commit
c2a7e40b67
6 changed files with 495 additions and 16 deletions
|
|
@ -40,6 +40,7 @@
|
|||
| workplan | CB-WP-0030 | done | — | workplans/CB-WP-0030-a-number-that-does-not-move.md |
|
||||
| workplan | CB-WP-0031 | done | — | workplans/CB-WP-0031-the-comment-box-outlives-the-game.md |
|
||||
| workplan | CB-WP-0032 | done | — | workplans/CB-WP-0032-the-comments-in-the-account.md |
|
||||
| workplan | CB-WP-0033 | done | — | workplans/CB-WP-0033-a-game-is-the-unit.md |
|
||||
| task | CB-WP-0001-T01 | done | — | workplans/CB-WP-0001-inner-loop.md |
|
||||
| task | CB-WP-0001-T02 | done | — | workplans/CB-WP-0001-inner-loop.md |
|
||||
| task | CB-WP-0001-T03 | done | — | workplans/CB-WP-0001-inner-loop.md |
|
||||
|
|
@ -203,3 +204,4 @@
|
|||
| task | CB-WP-0030-T04 | done | — | workplans/CB-WP-0030-a-number-that-does-not-move.md |
|
||||
| task | CB-WP-0031-T01 | done | — | workplans/CB-WP-0031-the-comment-box-outlives-the-game.md |
|
||||
| task | CB-WP-0032-T01 | done | — | workplans/CB-WP-0032-the-comments-in-the-account.md |
|
||||
| task | CB-WP-0033-T01 | done | — | workplans/CB-WP-0033-a-game-is-the-unit.md |
|
||||
|
|
|
|||
|
|
@ -889,17 +889,39 @@ fn player_card(out: &mut String, id: PlayerId, p: &PlayerView, view: &GroundView
|
|||
}
|
||||
);
|
||||
}
|
||||
// CB-WP-0034. These four were `{:?}` on a player surface, which is
|
||||
// the defect `selection_words` above was written for -- fixed once,
|
||||
// for one field, with its four neighbours left as they were.
|
||||
//
|
||||
// The `support` line is the one the maintainer reported: it read
|
||||
// `support AcceptBond`, which names the answer and not the person.
|
||||
if let Some(m) = view.ground_modes.get(&id) {
|
||||
let _ = write!(out, "<span class=\"k\">ground mode</span> {m:?}<br>");
|
||||
let _ = write!(
|
||||
out,
|
||||
"<span class=\"k\">ground mode</span> {}<br>",
|
||||
esc(ground_mode_label(m))
|
||||
);
|
||||
}
|
||||
if let Some(c) = view.ground_choices.get(&id) {
|
||||
let _ = write!(out, "<span class=\"k\">ground choice</span> {c:?}<br>");
|
||||
let _ = write!(
|
||||
out,
|
||||
"<span class=\"k\">ground choice</span> {}<br>",
|
||||
esc(&ground_choice_label(c))
|
||||
);
|
||||
}
|
||||
if let Some(r) = view.support_responses.get(&id) {
|
||||
let _ = write!(out, "<span class=\"k\">support</span> {r:?}<br>");
|
||||
let _ = write!(
|
||||
out,
|
||||
"<span class=\"k\">support</span> {}<br>",
|
||||
esc(&support_words(r, supporter_of(view, Some(id))))
|
||||
);
|
||||
}
|
||||
if let Some(t) = view.darvo_targets.get(&id) {
|
||||
let _ = write!(out, "<span class=\"k\">darvo target</span> {t:?}<br>");
|
||||
let _ = write!(
|
||||
out,
|
||||
"<span class=\"k\">darvo target</span> {}<br>",
|
||||
esc(&darvo_target_words(t))
|
||||
);
|
||||
}
|
||||
out.push_str("</div>");
|
||||
}
|
||||
|
|
@ -989,7 +1011,7 @@ pub fn document_with_log(
|
|||
// game rather than part of it.
|
||||
s.push_str("<div class=\"cb-cols\"><div class=\"cb-game\">");
|
||||
body(&mut s, view);
|
||||
move_section(&mut s, legal, seat, may_pass);
|
||||
move_section(&mut s, view, legal, seat, may_pass);
|
||||
s.push_str("</div><div class=\"cb-meta\">");
|
||||
meta_section(&mut s, meta, note_to);
|
||||
log_section(&mut s, log);
|
||||
|
|
@ -1114,9 +1136,19 @@ fn body(s: &mut String, view: &GroundView) {
|
|||
} else {
|
||||
o.coalitions
|
||||
.iter()
|
||||
.map(|c| format!("{c:?}"))
|
||||
.map(|c| {
|
||||
format!(
|
||||
"{} (score {})",
|
||||
c.members
|
||||
.iter()
|
||||
.map(|m| seat_name(*m))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" + "),
|
||||
c.score
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
.join(", ")
|
||||
};
|
||||
let _ = write!(
|
||||
s,
|
||||
|
|
@ -1159,8 +1191,175 @@ fn action_text(a: games_ground::Action) -> Option<games_ground::edition::CardTex
|
|||
.find(|c| c.id == want)
|
||||
}
|
||||
|
||||
/// Who offered this seat their Support (CB-WP-0034).
|
||||
///
|
||||
/// **Read off the view, because the command does not carry it.**
|
||||
/// `RespondToSupport { response }` names the answer and not the person,
|
||||
/// so no rendering of that command alone can say who — which is why the
|
||||
/// page could not say it either.
|
||||
fn supporter_of(view: &GroundView, seat: Option<PlayerId>) -> Option<PlayerId> {
|
||||
let me = seat?;
|
||||
view.selections.iter().find_map(|(p, sel)| match sel {
|
||||
games_ground::view::SelectionView::Shown(s)
|
||||
if s.action == games_ground::Action::Support && s.target == Some(me) =>
|
||||
{
|
||||
Some(*p)
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// The edition's names for the three GROUND modes (GR-A10..A12).
|
||||
fn ground_mode_label(m: &games_ground::GroundMode) -> &'static str {
|
||||
use games_ground::GroundMode as M;
|
||||
match m {
|
||||
M::Gr => "GROUND \u{2014} Ground & Restate",
|
||||
M::Ou => "GROUND \u{2014} Observe & Uphold",
|
||||
M::Nd => "GROUND \u{2014} Name & Decide",
|
||||
}
|
||||
}
|
||||
|
||||
/// A DARVO target, in words.
|
||||
fn darvo_target_words(t: &games_ground::DarvoTarget) -> String {
|
||||
match (t.player, t.problem) {
|
||||
(Some(p), Some(q)) => format!("{}, Problem {q}", seat_name(p)),
|
||||
(Some(p), None) => seat_name(p),
|
||||
(None, Some(q)) => format!("Problem {q}"),
|
||||
(None, None) => "nothing".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// What a seat answered a Support with, **and to whom**.
|
||||
fn support_words(r: &games_ground::SupportResponse, who: Option<PlayerId>) -> String {
|
||||
use games_ground::SupportResponse as R;
|
||||
// Branch on whether the seat is KNOWN before phrasing, rather than
|
||||
// substituting a noun phrase into a possessive: the first draft read
|
||||
// "accepted a seat this view cannot show's Support".
|
||||
let Some(who) = who.map(seat_name) else {
|
||||
let what = match r {
|
||||
R::AcceptBond => "accepted the Support offered \u{2014} formed a Bond",
|
||||
R::DeclineBond => "declined the Support offered",
|
||||
R::FlipToBond => "accepted the Support offered \u{2014} Rivalry became a Bond",
|
||||
R::BreakRivalry => "broke the Rivalry",
|
||||
};
|
||||
return format!("{what} (this view does not show which seat)");
|
||||
};
|
||||
match r {
|
||||
R::AcceptBond => format!("accepted {who}\u{2019}s Support \u{2014} Bond with {who}"),
|
||||
R::DeclineBond => format!("declined {who}\u{2019}s Support"),
|
||||
R::FlipToBond => format!("accepted {who}\u{2019}s Support \u{2014} Rivalry became a Bond"),
|
||||
R::BreakRivalry => format!("broke the Rivalry with {who}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// A GROUND sub-choice, in words, naming whoever it touches.
|
||||
fn ground_choice_label(c: &games_ground::GroundChoice) -> String {
|
||||
use games_ground::GroundChoice as G;
|
||||
match c {
|
||||
G::RestoreProblem { problem } => format!("restore Problem {problem}"),
|
||||
G::CancelAttack { attacker } => {
|
||||
format!("cancel {}\u{2019}s Attack on you", seat_name(*attacker))
|
||||
}
|
||||
G::ProtectProblem { problem } => format!("protect Problem {problem} from Deny"),
|
||||
G::RemoveBlame { owner } => format!("remove {}\u{2019}s Blame token", seat_name(*owner)),
|
||||
G::BreakRelation { with } => format!("break your relation with {}", seat_name(*with)),
|
||||
G::RejectReverse => "reject the Reverse aimed at you".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// A move button\u2019s words \u2014 and **who it is aimed at** (CB-WP-0034).
|
||||
///
|
||||
/// The label was `format!("{c:?}")`. A player deciding whether to accept a
|
||||
/// Bond read `RespondToSupport { response: AcceptBond }` \u2014 Rust struct
|
||||
/// syntax with no name anywhere in it \u2014 which is why *"i cant see whos
|
||||
/// support i accept"* was reported three times across three sessions and
|
||||
/// survived a whole UI rebuild.
|
||||
///
|
||||
/// **Exhaustive on purpose: there is no catch-all arm.** A new command
|
||||
/// must be given words or this crate stops compiling. A `_ =>` here is
|
||||
/// how `Debug` output comes back one variant at a time.
|
||||
///
|
||||
/// **These are clay-borg\u2019s words, not the edition\u2019s**, and the
|
||||
/// distinction matters (ADR-0015). The Action cards above carry the
|
||||
/// game\u2019s own `rules_text`; nothing in the vendored files names these
|
||||
/// control moves, and `Glossary` is still unvendored (F18). If it lands,
|
||||
/// this is a caller to revisit.
|
||||
fn command_label(
|
||||
c: &games_ground::GroundCommand,
|
||||
view: &GroundView,
|
||||
seat: Option<PlayerId>,
|
||||
) -> String {
|
||||
use games_ground::GroundCommand as C;
|
||||
match c {
|
||||
C::SelectAction {
|
||||
action,
|
||||
target,
|
||||
problem,
|
||||
} => {
|
||||
let mut out = format!("{action:?}").to_uppercase();
|
||||
if let Some(t) = target {
|
||||
let _ = write!(out, " on {}", seat_name(*t));
|
||||
}
|
||||
if let Some(p) = problem {
|
||||
let _ = write!(out, ", Problem {p}");
|
||||
}
|
||||
out
|
||||
}
|
||||
C::SpendFreedom => {
|
||||
"spend your Freedom token \u{2014} act despite the Stress gate".to_string()
|
||||
}
|
||||
C::ChooseGroundMode { mode, choice } => {
|
||||
let named = ground_mode_label(mode);
|
||||
match choice {
|
||||
Some(ch) => format!("{named}: {}", ground_choice_label(ch)),
|
||||
None => named.to_string(),
|
||||
}
|
||||
}
|
||||
// The one the report was about.
|
||||
C::RespondToSupport { response } => {
|
||||
use games_ground::SupportResponse as R;
|
||||
// Not a guess. If the offer is not visible in this view, the
|
||||
// page says so rather than inventing a name or quietly
|
||||
// dropping the question -- a confidently wrong seat is worse
|
||||
// than an honest gap (ADR-0018).
|
||||
let Some(who) = supporter_of(view, seat).map(seat_name) else {
|
||||
let what = match response {
|
||||
R::AcceptBond => "accept the Support offered to you \u{2014} form a Bond",
|
||||
R::DeclineBond => "decline the Support offered to you",
|
||||
R::FlipToBond => {
|
||||
"accept the Support offered to you \u{2014} turn your Rivalry into a Bond"
|
||||
}
|
||||
R::BreakRivalry => "break the Rivalry",
|
||||
};
|
||||
return format!("{what} (this view does not show which seat)");
|
||||
};
|
||||
match response {
|
||||
R::AcceptBond => {
|
||||
format!("accept {who}\u{2019}s Support \u{2014} form a Bond with {who}")
|
||||
}
|
||||
R::DeclineBond => {
|
||||
format!("decline {who}\u{2019}s Support \u{2014} the Stress still applies")
|
||||
}
|
||||
R::FlipToBond => {
|
||||
format!("accept {who}\u{2019}s Support \u{2014} turn your Rivalry into a Bond")
|
||||
}
|
||||
R::BreakRivalry => {
|
||||
format!("break your Rivalry with {who}")
|
||||
}
|
||||
}
|
||||
}
|
||||
C::ChooseDarvoTarget { target } => {
|
||||
format!("DARVO target: {}", darvo_target_words(target))
|
||||
}
|
||||
C::Reveal => "reveal all selections".to_string(),
|
||||
C::Resolve => "resolve the revealed Actions".to_string(),
|
||||
C::EndRound => "end the round".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn move_section(
|
||||
s: &mut String,
|
||||
view: &GroundView,
|
||||
legal: &[games_ground::GroundCommand],
|
||||
seat: Option<PlayerId>,
|
||||
may_pass: bool,
|
||||
|
|
@ -1233,7 +1432,7 @@ fn move_section(
|
|||
let _ = write!(
|
||||
s,
|
||||
"<div class=\"card btn tap\" data-drop=\"cmd-{i}\">{}</div>",
|
||||
esc(&format!("{c:?}"))
|
||||
esc(&command_label(c, view, seat))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -163,18 +163,28 @@ mod coverage {
|
|||
("selections.*.action", "selected Attack"),
|
||||
("selections.*.target", "Attack on P2"),
|
||||
("selections.*.problem", "for problem 7"),
|
||||
("ground_modes.*", "ground mode Gr"),
|
||||
("ground_choices.*.choice", "ProtectProblem"),
|
||||
("ground_choices.*.problem", "problem: 7 }"),
|
||||
("support_responses.*", "support AcceptBond"),
|
||||
("darvo_targets.*.problem", "problem: Some(1)"),
|
||||
("darvo_targets.*.player", "player: Some(PlayerId(1))"),
|
||||
// CB-WP-0034: words, not Debug — and these probes are the reason
|
||||
// it survived so long. They matched `problem: 7 }`,
|
||||
// `player: Some(PlayerId(1))` and `members: [PlayerId(1)`, so the
|
||||
// gate that exists to prove every field reaches the PLAYER was
|
||||
// proving it by matching Rust struct syntax. It certified the
|
||||
// defect as coverage.
|
||||
//
|
||||
// Second confirmation of CB-WP-0024's finding, from the other
|
||||
// side: a probe naming a PRESENTATION does not survive a
|
||||
// rendering change, and one naming Debug output actively pins it.
|
||||
("ground_modes.*", "Ground & Restate"),
|
||||
("ground_choices.*.choice", "protect Problem"),
|
||||
("ground_choices.*.problem", "Problem 7 from Deny"),
|
||||
("support_responses.*", "accepted P3\u{2019}s Support"),
|
||||
("darvo_targets.*.problem", "Problem 1"),
|
||||
("darvo_targets.*.player", "darvo target P2"),
|
||||
("outcome.total", "total 9"),
|
||||
("outcome.threshold", "of 12"),
|
||||
("outcome.group_success", "failure"),
|
||||
("outcome.personal.*", "P1 +3"),
|
||||
("outcome.coalitions.*.members.*", "members: [PlayerId(1)"),
|
||||
("outcome.coalitions.*.score", "score: 4"),
|
||||
("outcome.coalitions.*.members.*", "P2 + P3"),
|
||||
("outcome.coalitions.*.score", "(score 4)"),
|
||||
("outcome.mastery", "mastery +1"),
|
||||
("outcome.winners.*", "winners P1"),
|
||||
];
|
||||
|
|
@ -598,6 +608,151 @@ mod gamelog {
|
|||
}
|
||||
}
|
||||
|
||||
/// CB-WP-0034. The move button must name **who**.
|
||||
///
|
||||
/// Reported three times across three sessions, two days apart, and it
|
||||
/// survived a whole UI rebuild: *"RespondToSupport is unclear as i
|
||||
/// cant see whos support i accept"*, then twice more about bonding.
|
||||
///
|
||||
/// The cause was not styling. The label was `format!("{c:?}")`, so
|
||||
/// the button read `RespondToSupport { response: AcceptBond }` — Rust
|
||||
/// struct syntax with no name in it — and the command **does not
|
||||
/// carry** the counterparty, so nothing rendering the command alone
|
||||
/// could have said who. It comes off the view.
|
||||
#[test]
|
||||
fn a_support_response_names_the_seat_that_offered_it() {
|
||||
use games_ground::view::SelectionView;
|
||||
use games_ground::{Action, GroundCommand, Selection, SupportResponse};
|
||||
let me = PlayerId(0);
|
||||
let them = PlayerId(1);
|
||||
let mut v = crate::testfix::view(Some(me));
|
||||
// P2 played Support at P1, revealed.
|
||||
v.selections.insert(
|
||||
them,
|
||||
SelectionView::Shown(Selection {
|
||||
action: Action::Support,
|
||||
target: Some(me),
|
||||
problem: None,
|
||||
}),
|
||||
);
|
||||
let legal = vec![
|
||||
GroundCommand::RespondToSupport {
|
||||
response: SupportResponse::AcceptBond,
|
||||
},
|
||||
GroundCommand::RespondToSupport {
|
||||
response: SupportResponse::DeclineBond,
|
||||
},
|
||||
];
|
||||
let html = document_with_log(
|
||||
&v,
|
||||
&legal,
|
||||
crate::TEST_ENDPOINTS,
|
||||
Some(me),
|
||||
false,
|
||||
Account::of(&[]),
|
||||
&[],
|
||||
);
|
||||
let text = crate::text_of(&html);
|
||||
assert!(text.contains("accept P2"), "the offer is anonymous: {text}");
|
||||
assert!(
|
||||
text.contains("decline P2"),
|
||||
"the refusal is anonymous: {text}"
|
||||
);
|
||||
// The defect itself, named: no Rust struct syntax on a button.
|
||||
assert!(
|
||||
!text.contains("RespondToSupport"),
|
||||
"the raw command name is still on the page: {text}"
|
||||
);
|
||||
assert!(
|
||||
!text.contains("AcceptBond"),
|
||||
"the raw variant name is still on the page: {text}"
|
||||
);
|
||||
}
|
||||
|
||||
/// When the offer is not visible in this view, the page says so.
|
||||
///
|
||||
/// **It must not invent a name and must not drop the question.** A
|
||||
/// confident wrong seat is worse than an honest gap — that is the
|
||||
/// family ADR-0018 exists for.
|
||||
#[test]
|
||||
fn an_unseen_offer_is_not_given_an_invented_name() {
|
||||
use games_ground::{GroundCommand, SupportResponse};
|
||||
let me = PlayerId(0);
|
||||
let mut v = crate::testfix::view(Some(me));
|
||||
v.selections.clear();
|
||||
let legal = vec![GroundCommand::RespondToSupport {
|
||||
response: SupportResponse::AcceptBond,
|
||||
}];
|
||||
let text = crate::text_of(&document_with_log(
|
||||
&v,
|
||||
&legal,
|
||||
crate::TEST_ENDPOINTS,
|
||||
Some(me),
|
||||
false,
|
||||
Account::of(&[]),
|
||||
&[],
|
||||
));
|
||||
assert!(
|
||||
text.contains("this view does not show which seat"),
|
||||
"it should say the offer is not visible: {text}"
|
||||
);
|
||||
for name in ["P1", "P2", "P3"] {
|
||||
assert!(
|
||||
!text.contains(&format!("accept {name}")),
|
||||
"it named {name} with nothing to go on: {text}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// No move button may carry Rust `Debug` output (CB-WP-0034).
|
||||
///
|
||||
/// The guard against this returning is that `command_label` has no
|
||||
/// catch-all arm, so a new variant stops the build. This asserts the
|
||||
/// visible half over every command the fixture can offer.
|
||||
#[test]
|
||||
fn no_move_button_shows_rust_struct_syntax() {
|
||||
use games_ground::*;
|
||||
let me = PlayerId(0);
|
||||
let legal = vec![
|
||||
GroundCommand::SpendFreedom,
|
||||
GroundCommand::ChooseGroundMode {
|
||||
mode: GroundMode::Ou,
|
||||
choice: Some(GroundChoice::ProtectProblem { problem: 7 }),
|
||||
},
|
||||
GroundCommand::ChooseDarvoTarget {
|
||||
target: DarvoTarget {
|
||||
problem: Some(1),
|
||||
player: Some(PlayerId(1)),
|
||||
},
|
||||
},
|
||||
GroundCommand::RespondToSupport {
|
||||
response: SupportResponse::FlipToBond,
|
||||
},
|
||||
];
|
||||
let text = crate::text_of(&document_with_log(
|
||||
&crate::testfix::view(Some(me)),
|
||||
&legal,
|
||||
crate::TEST_ENDPOINTS,
|
||||
Some(me),
|
||||
false,
|
||||
Account::of(&[]),
|
||||
&[],
|
||||
));
|
||||
// The tells of a `{:?}` label reaching a player.
|
||||
for tell in ["{ ", " }", "Some(", "None", "PlayerId("] {
|
||||
assert!(
|
||||
!text.contains(tell),
|
||||
"Debug output {tell:?} reached the page: {text}"
|
||||
);
|
||||
}
|
||||
// And the words are actually there. `SpendFreedom` is not among
|
||||
// them: it carries a spatial affordance, so it is a drag target
|
||||
// rather than a button -- which is why this asserts on the
|
||||
// commands that DO become buttons.
|
||||
assert!(text.contains("Observe & Uphold"), "{text}");
|
||||
assert!(text.contains("DARVO target: P2, Problem 1"), "{text}");
|
||||
}
|
||||
|
||||
fn note(after: usize, text: &str) -> crate::doc::LogNote {
|
||||
crate::doc::LogNote {
|
||||
after,
|
||||
|
|
|
|||
|
|
@ -89,6 +89,18 @@ pub fn view(viewer: Option<PlayerId>) -> GroundView {
|
|||
}),
|
||||
),
|
||||
(p2, SelectionView::Hidden),
|
||||
// CB-WP-0034: P3 offered P2 their Support, revealed. The
|
||||
// fixture now carries the case the report was about, so the
|
||||
// coverage probe for `support_responses` can name a SEAT
|
||||
// rather than a response with nobody attached to it.
|
||||
(
|
||||
p3,
|
||||
SelectionView::Shown(Selection {
|
||||
action: Action::Support,
|
||||
target: Some(p2),
|
||||
problem: None,
|
||||
}),
|
||||
),
|
||||
]),
|
||||
ground_modes: BTreeMap::from([(p3, GroundMode::Gr)]),
|
||||
ground_choices: BTreeMap::from([(p3, GroundChoice::ProtectProblem { problem: 7 })]),
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ id: CB-WP-0033
|
|||
kind: product
|
||||
title: "A game is the unit"
|
||||
status: done
|
||||
state_hub_workstream_id: "66c54551-38cd-465e-bb2f-9e1d83e14438"
|
||||
---
|
||||
|
||||
# Purpose
|
||||
|
|
@ -51,6 +52,7 @@ session still destroyed its own evidence.** [ADR-0019](../decisions/ADR-0019-a-g
|
|||
id: CB-WP-0033-T01
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "b0b4b9f3-f99f-4b4d-8eed-ba2fa4f9ea98"
|
||||
```
|
||||
|
||||
**Controls:**
|
||||
|
|
|
|||
109
workplans/CB-WP-0034-who-you-are-bonding-with.md
Normal file
109
workplans/CB-WP-0034-who-you-are-bonding-with.md
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
---
|
||||
id: CB-WP-0034
|
||||
kind: product
|
||||
title: "Who you are bonding with"
|
||||
status: done
|
||||
---
|
||||
|
||||
# Purpose
|
||||
|
||||
```
|
||||
structural tier S (rendering only -- no rule, no dependency, no budget,
|
||||
no artifact contract moved)
|
||||
chaos d8 = 4 → no override
|
||||
declared tier S
|
||||
```
|
||||
|
||||
**Declaration 5 of chaos window 3.**
|
||||
|
||||
## The report, three times
|
||||
|
||||
> *"RespondToSupport is unclear as i cant see whos support i accept."*
|
||||
> — 2026-08-06
|
||||
>
|
||||
> *"i can not [see] the player i bond or not-bond with. We should change
|
||||
> that."* — 2026-08-07 15:58
|
||||
>
|
||||
> *"But i **still** cant see the player i am bonding with."* — 2026-08-07 16:12
|
||||
|
||||
Three sessions, two days, and it **survived a whole UI rebuild**.
|
||||
|
||||
## It was not styling
|
||||
|
||||
The move button's label was `format!("{c:?}")`. The player was reading
|
||||
|
||||
```
|
||||
RespondToSupport { response: AcceptBond }
|
||||
```
|
||||
|
||||
— Rust struct syntax, with **no name anywhere in it**. And
|
||||
`GroundCommand::RespondToSupport { response }` **does not carry the
|
||||
counterparty**, so nothing rendering the command alone could have named
|
||||
them. It comes off the view: whoever played `Support` targeting this seat.
|
||||
|
||||
**Four more fields on the seat panel had the same defect**, including the
|
||||
one the report names — `support AcceptBond`, an answer with nobody
|
||||
attached. CB-WP-0020 fixed exactly this for `selections` and left its four
|
||||
neighbours as they were.
|
||||
|
||||
## Task: name who
|
||||
|
||||
```task
|
||||
id: CB-WP-0034-T01
|
||||
status: done
|
||||
priority: high
|
||||
```
|
||||
|
||||
**Controls:**
|
||||
- **the seat is named**, and the raw command name never reaches the page;
|
||||
- **an unseen offer is not given an invented name** — a confidently wrong
|
||||
seat is worse than an honest gap (ADR-0018);
|
||||
- **no catch-all arm**, so a new command cannot silently fall back to
|
||||
`Debug`;
|
||||
- mutation-proven both ways.
|
||||
|
||||
**Done 2026-08-07.**
|
||||
|
||||
**The exhaustiveness control paid for itself immediately**, before any
|
||||
test ran: `GroundChoice::RejectReverse` and
|
||||
`SupportResponse::BreakRivalry` both failed to compile. Two moves that
|
||||
would have shipped as struct dumps, caught by the absence of a `_ =>`.
|
||||
|
||||
**These are clay-borg's words, not the edition's.** The Action cards carry
|
||||
the game's own `rules_text` (ADR-0015); nothing vendored names these
|
||||
control moves, and `Glossary` is unvendored (F18). If it lands, this is a
|
||||
caller to revisit.
|
||||
|
||||
## The finding underneath: a gate that pinned the defect
|
||||
|
||||
`every_view_field_is_classified_in_the_emitted_document` — the probe that
|
||||
exists to prove **every view field reaches the player** — was matching
|
||||
|
||||
```
|
||||
"problem: 7 }" "player: Some(PlayerId(1))" "members: [PlayerId(1)"
|
||||
```
|
||||
|
||||
**It was proving the field reached the page by matching Rust struct
|
||||
syntax.** A gate written to protect the player's view of the game had
|
||||
been certifying the defect as coverage, and would have gone red if
|
||||
anyone had fixed it.
|
||||
|
||||
**Second confirmation of CB-WP-0024's finding, from the other side.** That
|
||||
pass found a probe naming a *presentation* does not survive a rendering
|
||||
change. This is the sharper case: a probe naming *Debug output* actively
|
||||
holds the defect in place.
|
||||
|
||||
Probes now name facts — `Ground & Restate`, `P2 + P3`, `accepted P3's
|
||||
Support`. The fixture gained a visible Support offer so the probe can name
|
||||
a seat rather than a response with nobody attached.
|
||||
|
||||
## Not done here
|
||||
|
||||
- **The unknown-supporter path is untested against a real game.** It is
|
||||
reachable only if `RespondToSupport` is offered while the Support is not
|
||||
visible in the responder's view; whether the rules permit that is a
|
||||
question for `ground-game`, and the page currently says it cannot see
|
||||
rather than guessing.
|
||||
- **Other Debug output may remain outside the two surfaces audited here**
|
||||
(the move buttons and the seat panel). The exhaustive match guards
|
||||
commands only.
|
||||
Loading…
Add table
Add a link
Reference in a new issue