CB-WP-0046: the rule the placement encodes, and a log that names its rules
Some checks failed
ci / check (push) Failing after 4s

CB-WP-0045 left "nothing explains what a scope does" open and gave a
FALSE reason: that scoping is our Variant so no printed sentence exists.
The H2 package ships Rules_Text.csv -- twenty-two passages of
player-facing rules -- and nothing in clay-borg had ever read the file.
A wrong reason for an open item is worse than an open item; it retires
the question. Filed as F26 against ground-game: a package that adds a
FILE is invisible where one that adds a column is not.

The scope rule now renders under the table in the edition's own words,
only when a non-global scope is in play, matched by heading rather than
row number, and absent (never paraphrased) if the edition drops it.

The trial log stamps its variant on the begin marker -- a session
property, not an eighth column -- read off state.variant rather than the
--variant flag, because a bare `state.variant = v` leaves H2 inert and a
flag-stamped log would put false provenance on real player words. An
unstamped log reports `unrecorded`, never `ground-darvo-r0`.

Six mutations, six red. The sixth is the finding: every trials.py fixture
built its marker out of BEGIN, so nine checks followed BEGIN away from
what hotseat.rs writes and stayed green while real logs broke. A fixture
built from the constant under test cannot test the constant -- the
control is now a literal, asserted from both sides.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-08-08 19:15:52 +02:00
parent df65e2dee1
commit de25c4cae0
10 changed files with 986 additions and 12 deletions

View file

@ -1306,6 +1306,25 @@ fn body(s: &mut String, view: &GroundView) {
s.push_str("<div class=\"card\">no problems in play</div>");
}
s.push_str(&table_svg(view));
// CB-WP-0046: the placement is legible, the RULE it encodes was not.
//
// Shown only when a non-global scope is actually on the table — under
// the baseline every Problem is everyone's, and an explanation of a
// distinction that is not in play is noise. Same discipline as the
// GROUND modes: at the thing it explains, and closable.
if view.problem_markers.values().any(|m| {
m.scope
.is_some_and(|x| x != games_ground::edition::StressScope::Global)
}) {
if let Some(rule) = stress_scope_rule_text() {
let _ = write!(
s,
"<details class=\"card\"><summary>what a Problem\u{2019}s scope does\
</summary>{}</details>",
esc(&rule)
);
}
}
s.push_str("<h2>seats</h2><div class=\"row\">");
for (id, p) in &view.players {
@ -1490,6 +1509,20 @@ fn ground_modes_text() -> Option<String> {
.map(|c| c.rules_text)
}
/// What a scope DOES, in the edition's words (CB-WP-0046).
///
/// CB-WP-0045 left this open and gave the wrong reason: *"the scope rule
/// is ours (a Variant), so there is no vendored sentence to render."* The
/// H2 package ships `Rules_Text.csv` and its `Problems / Stress scope`
/// passage is exactly the rule. Nothing read the file.
///
/// **The placement was legible and the rule it encodes was not** — a
/// player could see a card sitting on their Bond lines and have no way to
/// learn that its Stress presses everyone on those lines.
fn stress_scope_rule_text() -> Option<String> {
games_ground::edition::stress_scope_rule()
}
/// 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;

View file

@ -697,6 +697,65 @@ mod gamelog {
}
}
/// **A scope says what it does** (CB-WP-0046).
///
/// CB-WP-0045 shipped the placement and recorded the gap with the
/// wrong reason — that scoping is our Variant so no printed sentence
/// exists. `Rules_Text.csv` was in the H2 package the whole time and
/// nothing read it. A player could see a card on their Bond lines
/// and have no way to learn what that meant.
#[test]
fn a_scoped_table_says_what_a_scope_does() {
let mut v = crate::testfix::view(Some(PlayerId(0)));
v.variant = games_ground::Variant::H2ScopedProblemStress;
// One card that is not everyone's — the condition for the rule
// to be worth stating at all.
if let Some(m) = v.problem_markers.values_mut().next() {
m.scope = Some(games_ground::edition::StressScope::Bond);
m.owner = Some(PlayerId(1));
}
let text = crate::text_of(&crate::doc::document(
&v,
&[],
"/c",
Some(PlayerId(0)),
false,
));
assert!(
text.contains("scope does"),
"Problems are placed by scope and nothing says what a scope does"
);
// The EDITION's sentence. A paraphrase of ours would drift from
// the rule the kernel implements (ADR-0015).
let rule = games_ground::edition::stress_scope_rule()
.expect("h2 Rules_Text.csv carries the scope rule");
let head: String = rule.chars().take(60).collect();
assert!(
text.contains(&head),
"the scope explanation is not the edition's own words: {head:?}"
);
}
/// The baseline is not told about a distinction it does not have.
#[test]
fn an_unscoped_table_does_not_explain_scopes() {
let mut v = crate::testfix::view(Some(PlayerId(0)));
for m in v.problem_markers.values_mut() {
m.scope = Some(games_ground::edition::StressScope::Global);
}
let text = crate::text_of(&crate::doc::document(
&v,
&[],
"/c",
Some(PlayerId(0)),
false,
));
assert!(
!text.contains("scope does"),
"the baseline table explains a scope distinction that is not in play"
);
}
/// **The number is on the card** (CB-WP-0045).
///
/// Every move label says "Problem 7"; nothing on the table said which