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

@ -100,7 +100,11 @@ pub fn game_path(base: &std::path::Path, n: usize) -> std::path::PathBuf {
/// **Reusing `FindingRegister.md`'s idiom deliberately** (ADR-0014 D2) —
/// a table between HTML-comment markers, so a human reads the file and a
/// tool reads the block, and neither needs the other's cooperation.
pub fn write_trial_log(path: &std::path::Path, notes: &[TrialNote]) -> Result<(), String> {
pub fn write_trial_log(
path: &std::path::Path,
notes: &[TrialNote],
variant: &str,
) -> Result<(), String> {
let mut s = String::new();
s.push_str("# Trial log\n\n");
s.push_str(
@ -111,7 +115,19 @@ pub fn write_trial_log(path: &std::path::Path, notes: &[TrialNote]) -> Result<()
`ground-game` (ADR-0014 D4) a note reaches them only by being promoted to a\n\
register finding, by a human, with the wording chosen then.\n\n",
);
s.push_str("<!-- trial-log:begin -->\n\n");
// CB-WP-0046: WHICH RULES THIS SESSION PLAYED.
//
// Without it a baseline note and an H2 note are indistinguishable,
// which stopped being survivable the moment both were being played
// in the same week. It rides the marker rather than becoming an
// eighth column because the variant is a property of the SESSION,
// and a per-note column would repeat one fact on every line.
//
// Read off the STATE, not off the command line: `--variant h2` with
// a bare `state.variant = v` assignment leaves H2 inert, and a log
// stamped from the flag would have recorded H2 for a session that
// played the baseline. This records what actually ran.
let _ = write!(s, "<!-- trial-log:begin variant={variant} -->\n\n");
// ADR-0019 D3/D4: `game` and `after` are the binding a reader can act
// on -- replay `after` commands of game `game` and you are standing
// where the player stood. `state_hash` stays as an integrity check at
@ -209,7 +225,7 @@ impl Server {
state_hash: hash[..12].to_string(),
text: note.text.clone(),
});
write_trial_log(path, &log)
write_trial_log(path, &log, state.variant.id())
}
/// Begin the next game (ADR-0019 D1).
@ -968,6 +984,63 @@ mod tests {
/// this loop breaks it; a player writing what they thought must be
/// able to write a second one and then still press `play again`,
/// which is what the trailing command here proves.
/// **The log says which rules the session played** (CB-WP-0046).
///
/// Without it a baseline note and an H2 note are indistinguishable,
/// which is how *"i did not notice any difference"* stays unreadable
/// after the fact — you cannot tell whether it is a finding about H2
/// or a note about a session that never ran it.
#[test]
fn the_trial_log_records_which_rules_were_played() {
let dir = std::env::temp_dir().join(format!("cb-trial-var-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("trial.md");
let note = TrialNote {
n: 1,
game: 1,
after: 4,
round: 3,
step: "Select".into(),
state_hash: "abc123def456".into(),
text: "it felt harder".into(),
};
write_trial_log(&path, &[note], "h2-scoped-problem-stress").unwrap();
let written = std::fs::read_to_string(&path).unwrap();
assert!(
written.contains("<!-- trial-log:begin variant=h2-scoped-problem-stress -->"),
"the log does not say which rules were played:\n{written}"
);
// **Not an eighth column.** The variant is a property of the
// session; repeating it per row would widen a table whose parser
// treats an unexpected width as an error.
let row = written
.lines()
.find(|l| l.starts_with("| 1 |"))
.expect("the note row");
assert_eq!(
row.matches('|').count() - 1,
7,
"the table gained a column: {row}"
);
let _ = std::fs::remove_dir_all(&dir);
}
/// The stamp is read off the STATE, never off the command line.
///
/// Three call sites once set `state.variant = v` directly instead of
/// `with_variant()`, which renders H2 inert. A log stamped from the
/// `--variant` flag would have recorded `h2` for a session that
/// played the baseline — a false provenance on real player words,
/// which is worse than no provenance.
#[test]
fn the_stamp_names_the_rules_that_actually_ran() {
let src = include_str!("hotseat.rs");
assert!(
src.contains("write_trial_log(path, &log, state.variant.id())"),
"the trial log is stamped from something other than the state"
);
}
#[test]
fn a_note_can_be_written_after_the_game_has_ended() {
let dir = std::env::temp_dir().join(format!("cb-note-end-{}", std::process::id()));