Compare commits
2 commits
fd76db4f71
...
de25c4cae0
| Author | SHA1 | Date | |
|---|---|---|---|
| de25c4cae0 | |||
| df65e2dee1 |
12 changed files with 990 additions and 12 deletions
|
|
@ -52,6 +52,7 @@
|
|||
| workplan | CB-WP-0042 | done | — | workplans/CB-WP-0042-h2-scoped-problem-stress.md |
|
||||
| workplan | CB-WP-0043 | done | — | workplans/CB-WP-0043-a-problem-sits-where-its-stress-lands.md |
|
||||
| workplan | CB-WP-0044 | done | — | workplans/CB-WP-0044-the-page-says-which-rules-it-plays.md |
|
||||
| workplan | CB-WP-0045 | done | — | workplans/CB-WP-0045-the-table-names-what-it-shows.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 |
|
||||
|
|
@ -243,3 +244,4 @@
|
|||
| task | CB-WP-0042-T05 | done | — | workplans/CB-WP-0042-h2-scoped-problem-stress.md |
|
||||
| task | CB-WP-0043-T01 | done | — | workplans/CB-WP-0043-a-problem-sits-where-its-stress-lands.md |
|
||||
| task | CB-WP-0044-T01 | done | — | workplans/CB-WP-0044-the-page-says-which-rules-it-plays.md |
|
||||
| task | CB-WP-0045-T01 | done | — | workplans/CB-WP-0045-the-table-names-what-it-shows.md |
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -82,6 +82,16 @@ const GLOSSARY_CSV: &str = include_str!("../../../editions/ground-darvo-r0/Gloss
|
|||
/// H2's Problems table (CB-WP-0042). r0's with one column added.
|
||||
const H2_PROBLEMS_CSV: &str =
|
||||
include_str!("../../../editions/experiments/h2-scoped-problem-stress/Problems.csv");
|
||||
/// H2's player-facing rulebook. **It was here all along** (CB-WP-0046).
|
||||
///
|
||||
/// CB-WP-0045 recorded that nothing explains what a *scope does* and
|
||||
/// judged it a `ground-game` question, on the reasoning that scoping is
|
||||
/// clay-borg's Variant and so has no printed sentence. That was wrong:
|
||||
/// the package ships `Rules_Text.csv`, and row 20 is the rule in the
|
||||
/// edition's own words. **We had never read the file** — the third time
|
||||
/// this pass that vendored text could not reach a player (F18).
|
||||
const H2_RULES_TEXT_CSV: &str =
|
||||
include_str!("../../../editions/experiments/h2-scoped-problem-stress/Rules_Text.csv");
|
||||
|
||||
/// A vendored CSV, parsed into rows addressable by column name.
|
||||
///
|
||||
|
|
@ -562,6 +572,59 @@ pub fn stress_scopes(scenario_id: &str) -> Result<BTreeMap<u8, StressScope>, Str
|
|||
Ok(out)
|
||||
}
|
||||
|
||||
/// One passage of the variant's rulebook (CB-WP-0046).
|
||||
pub struct RulesPassage {
|
||||
pub order: u32,
|
||||
pub section: String,
|
||||
pub heading: String,
|
||||
pub body: String,
|
||||
}
|
||||
|
||||
/// H2's rulebook, in `Rules_Text.csv` order.
|
||||
///
|
||||
/// Returned whole rather than as a lookup by heading: a caller that wants
|
||||
/// the scope rule asks for its section, and the ordering is the
|
||||
/// edition's, not one we impose.
|
||||
pub fn h2_rules_text() -> Result<Vec<RulesPassage>, String> {
|
||||
let t = Table::parse(H2_RULES_TEXT_CSV, "h2/Rules_Text.csv")?;
|
||||
let mut out = Vec::new();
|
||||
for row in &t.rows {
|
||||
out.push(RulesPassage {
|
||||
order: t
|
||||
.get(row, "order")?
|
||||
.trim()
|
||||
.parse()
|
||||
.map_err(|_| "order is not a number".to_string())?,
|
||||
section: t.get(row, "section")?.to_string(),
|
||||
heading: t.get(row, "heading")?.to_string(),
|
||||
body: t.get(row, "body")?.to_string(),
|
||||
});
|
||||
}
|
||||
if out.is_empty() {
|
||||
return Err("h2/Rules_Text.csv has no rows".into());
|
||||
}
|
||||
out.sort_by_key(|p| p.order);
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// What a `stress_scope` DOES, in the edition's words (CB-WP-0046).
|
||||
///
|
||||
/// The passage headed *Stress scope* in the `Problems` section. Matched
|
||||
/// on the heading rather than pinned to row 20, because a row number is
|
||||
/// a property of today's file and the heading is a property of the rule.
|
||||
/// **`None` if the edition stops carrying it** — an absent explanation is
|
||||
/// reported as absent, never replaced by ours (ADR-0018).
|
||||
pub fn stress_scope_rule() -> Option<String> {
|
||||
h2_rules_text()
|
||||
.ok()?
|
||||
.into_iter()
|
||||
.find(|p| {
|
||||
p.section.eq_ignore_ascii_case("Problems")
|
||||
&& p.heading.to_lowercase().contains("stress scope")
|
||||
})
|
||||
.map(|p| p.body)
|
||||
}
|
||||
|
||||
/// The stress gate, printed on every player mat (CB-WP-0037 T03).
|
||||
///
|
||||
/// **A mat is mostly ornamentation with one rule on it.** Symbol, colour
|
||||
|
|
|
|||
|
|
@ -53,9 +53,30 @@ kinds, states and metrics: [`GameDesign.md`](GameDesign.md). Reported by
|
|||
| F21 | degenerate | note | — | — | 2026-08-06 | clay-borg |
|
||||
| F23 | inconsistent | applied | decisions/ADR-0017-chaos-window-2-verdict.md | counterexample | 2026-08-07 | clay-borg |
|
||||
| F22 | underdetermined | withdrawn | games_ground::edition::supply_tests::play_never_exceeds_the_components_the_box_holds | counterexample | 2026-08-07 | clay-borg |
|
||||
| F26 | inert | raised | `crates/cb-render-html/src/lib.rs::a_scoped_table_says_what_a_scope_does` | counterexample | 2026-08-08 | ground-game |
|
||||
|
||||
<!-- design-register:end -->
|
||||
|
||||
- **F26 — a package that adds a FILE is invisible, where a package that
|
||||
adds a column is not.** `h2-scoped-problem-stress` ships
|
||||
`Rules_Text.csv` — twenty-two passages of player-facing rules, including
|
||||
the one passage that says what a `stress_scope` does. No reader in
|
||||
clay-borg mentioned the file, so `edition-check` could not call it stale
|
||||
(a file nothing reads is not out of date, it is unseen) and no gate
|
||||
could call it unread. The rule the table's whole layout encodes sat in
|
||||
the repo, reachable by no player, for the entire measured life of H2.
|
||||
|
||||
**Inert rather than underdetermined**: nothing is ambiguous about the
|
||||
rule and the engine implements it correctly — the text simply could not
|
||||
fire. Third instance of F18's shape and the first where the unit is a
|
||||
file, which is why it is filed separately instead of folded in.
|
||||
|
||||
Raised against `ground-game` because the actionable half is theirs: a
|
||||
package's manifest should name its own files, so that a consumer reading
|
||||
none of them is a detectable state. clay-borg now reads the one passage
|
||||
it needed (CB-WP-0046); the other twenty-one remain unread and that is
|
||||
the standing evidence.
|
||||
|
||||
- **F11 — SOLVE offered where it cannot act.** Offered on a face-down
|
||||
Problem, or with no matching suit in hand; inert every time. Ruled
|
||||
GROUND-WP-0002 T02, implemented CB-WP-0023 as GR-P05. `applied` — the
|
||||
|
|
|
|||
|
|
@ -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()));
|
||||
|
|
|
|||
123
tools/trials.py
123
tools/trials.py
|
|
@ -15,14 +15,27 @@ reuses `FindingRegister.md`'s idiom: a table between HTML-comment markers
|
|||
inside a readable Markdown file.
|
||||
"""
|
||||
|
||||
import os, re, sys, glob, datetime
|
||||
import os
|
||||
import re, sys, glob, datetime
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
TRIALS = os.path.join(ROOT, "trials")
|
||||
|
||||
BEGIN = "<!-- trial-log:begin -->"
|
||||
# The begin marker carries attributes since CB-WP-0046, so it is matched
|
||||
# by PREFIX. A bare `<!-- trial-log:begin -->` is still a valid marker —
|
||||
# every log written before this one has it.
|
||||
BEGIN = "<!-- trial-log:begin"
|
||||
END = "<!-- trial-log:end -->"
|
||||
|
||||
# Which rules the session was played under (CB-WP-0046).
|
||||
#
|
||||
# **A session property, not a note property**, so it rides the marker
|
||||
# rather than becoming an eighth column. The variant is fixed at startup
|
||||
# and cannot change between games, so a per-row column would repeat one
|
||||
# fact on every line and force a third accepted row width on a parser
|
||||
# whose docstring is about exactly that hazard.
|
||||
VARIANT_ATTR = re.compile(r"variant=([A-Za-z0-9._-]+)")
|
||||
|
||||
# GameDesign §3.1's figure, shared rather than reinvented: a note that
|
||||
# cannot expire ages into an apparent finding.
|
||||
NOTE_EXPIRY_DAYS = 30
|
||||
|
|
@ -45,7 +58,12 @@ def parse(text):
|
|||
is precisely the failure this tool exists to prevent."""
|
||||
if BEGIN not in text or END not in text:
|
||||
raise ValueError("no trial-log block")
|
||||
block = text.split(BEGIN)[1].split(END)[0]
|
||||
# Split off the marker's own line so an attribute cannot be read as
|
||||
# a table row, and so `variant=` is available to every row below it.
|
||||
tail = text.split(BEGIN, 1)[1].split(END)[0]
|
||||
marker, _, block = tail.partition("\n")
|
||||
hit = VARIANT_ATTR.search(marker)
|
||||
variant = hit.group(1) if hit else None
|
||||
rows = []
|
||||
for line in block.splitlines():
|
||||
line = line.strip()
|
||||
|
|
@ -63,16 +81,21 @@ def parse(text):
|
|||
# considered, and did not do) would have emptied every existing
|
||||
# log without a word.
|
||||
if len(cells) == len(COLUMNS):
|
||||
rows.append(dict(zip(COLUMNS, cells)))
|
||||
row = dict(zip(COLUMNS, cells))
|
||||
elif len(cells) == len(LEGACY_COLUMNS):
|
||||
row = dict(zip(LEGACY_COLUMNS, cells))
|
||||
row["legacy"] = True
|
||||
rows.append(row)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"trial-log row has {len(cells)} columns, expected "
|
||||
f"{len(COLUMNS)} or {len(LEGACY_COLUMNS)}: {line!r}"
|
||||
)
|
||||
# **`None`, not a default of "baseline"** — a log written before
|
||||
# the marker carried the attribute does not say which rules were
|
||||
# played, and answering an unrecorded question with the common
|
||||
# case is how a baseline note gets read as an H2 result.
|
||||
row["variant"] = variant
|
||||
rows.append(row)
|
||||
return rows
|
||||
|
||||
|
||||
|
|
@ -174,12 +197,20 @@ def report(root=TRIALS, today=None):
|
|||
found = logs(root)
|
||||
print("trials — what the players said, and where\n")
|
||||
total, orphans, pending, expired, legacy = 0, 0, 0, 0, 0
|
||||
by_variant = {}
|
||||
for path, rows, _ in found:
|
||||
name = os.path.basename(path)
|
||||
age = age_days(path, today)
|
||||
print(f" {name} ({len(rows)} note(s), {age}d)")
|
||||
# CB-WP-0046: which rules were played. `unrecorded` is said out
|
||||
# loud rather than filled in with the common case — a note read
|
||||
# under the wrong rules is worse than a note whose rules are
|
||||
# openly unknown.
|
||||
rules = (rows[0].get("variant") if rows else None) or "unrecorded"
|
||||
print(f" {name} ({len(rows)} note(s), {age}d, rules {rules})")
|
||||
for r in rows:
|
||||
total += 1
|
||||
key = r.get("variant") or "unrecorded"
|
||||
by_variant[key] = by_variant.get(key, 0) + 1
|
||||
# ADR-0019 D2: each note names its own game, and each game has
|
||||
# its own recording. One recording per FILE was the assumption
|
||||
# that made `play again` overwrite the evidence.
|
||||
|
|
@ -207,6 +238,15 @@ def report(root=TRIALS, today=None):
|
|||
print(f" legacy binding {legacy}"
|
||||
" (written before ADR-0019; position not checkable, NOT orphaned)")
|
||||
print(f" notes past {NOTE_EXPIRY_DAYS} days {expired} target 0")
|
||||
# A tally by rules, because the question a reader now has is "was this
|
||||
# said about H2 or about the baseline" and counting the logs by hand
|
||||
# is how that gets answered wrong.
|
||||
if by_variant:
|
||||
print(" notes by rules "
|
||||
+ ", ".join(f"{v} {n}" for v, n in sorted(by_variant.items())))
|
||||
if "unrecorded" in by_variant:
|
||||
print(" (unrecorded = written before"
|
||||
" CB-WP-0046 stamped the marker)")
|
||||
if total and not orphans and not legacy:
|
||||
print("\n Every note points at a position a reader can reach.")
|
||||
print("\n Raw notes stay in this repo (ADR-0014 D4). A note reaches"
|
||||
|
|
@ -231,13 +271,19 @@ def self_test():
|
|||
ok = ok and bool(cond)
|
||||
print(f" [{'ok ' if cond else 'FAIL'}] {name}" + (f" — {detail}" if detail else ""))
|
||||
|
||||
def log_of(*rows):
|
||||
# `BEGIN` is a PREFIX since CB-WP-0046, so a fixture has to close the
|
||||
# marker itself — which is the point: the closing text is what the
|
||||
# attribute sits inside.
|
||||
OPEN = BEGIN + " -->"
|
||||
|
||||
def log_of(*rows, marker=None):
|
||||
head = ("| n | game | after | round | step | state_hash | comment |\n"
|
||||
"|---|---|---|---|---|---|---|\n")
|
||||
return f"# Trial log\n\n{BEGIN}\n\n{head}" + "".join(rows) + f"\n{END}\n"
|
||||
return (f"# Trial log\n\n{marker or OPEN}\n\n{head}"
|
||||
+ "".join(rows) + f"\n{END}\n")
|
||||
|
||||
good = log_of("| 1 | 1 | 4 | 3 | Select | abc123def456 | why is SOLVE doing nothing |\n")
|
||||
legacy = (f"# Trial log\n\n{BEGIN}\n\n"
|
||||
legacy = (f"# Trial log\n\n{OPEN}\n\n"
|
||||
"| n | round | step | state_hash | comment |\n|---|---|---|---|---|\n"
|
||||
"| 1 | 3 | Select | abc123def456 | why is SOLVE doing nothing |\n"
|
||||
f"\n{END}\n")
|
||||
|
|
@ -256,6 +302,65 @@ def self_test():
|
|||
check("a legacy row is MARKED legacy", parse(legacy)[0].get("legacy") is True)
|
||||
check("the header row is not a note", all(r["n"] != "n" for r in parse(good)))
|
||||
|
||||
# CB-WP-0046 — WHICH RULES THE SESSION PLAYED.
|
||||
row = "| 1 | 1 | 4 | 3 | Select | abc123def456 | it felt harder |\n"
|
||||
stamped = log_of(row, marker=BEGIN + " variant=h2-scoped-problem-stress -->")
|
||||
check("a stamped log reports its variant",
|
||||
parse(stamped)[0]["variant"] == "h2-scoped-problem-stress")
|
||||
check("an unstamped log says NOTHING, not 'baseline'",
|
||||
parse(log_of(row))[0]["variant"] is None,
|
||||
"answering an unrecorded question with the common case is how a "
|
||||
"baseline note gets read as an H2 result")
|
||||
# The attribute must not be mistaken for a table row, and adding it
|
||||
# must not change how many notes the log has.
|
||||
check("the marker is not counted as a note", len(parse(stamped)) == 1)
|
||||
check("stamping does not disturb the row", parse(stamped)[0]["after"] == "4")
|
||||
# The marker is not a THIRD row width: the table shape is untouched,
|
||||
# which is why the parser needed no new accepted width.
|
||||
check("the stamped table has the same columns as the unstamped one",
|
||||
set(parse(stamped)[0]) - {"variant"} == set(parse(good)[0]) - {"variant"})
|
||||
# **A LITERAL marker, not one built from `BEGIN`.**
|
||||
#
|
||||
# Every fixture above composes its marker out of `BEGIN`, so all of
|
||||
# them follow `BEGIN` wherever it goes — including away from what
|
||||
# `hotseat.rs` actually writes. Reverting `BEGIN` to an exact match
|
||||
# broke real stamped logs and every one of those checks stayed green.
|
||||
# The format is shared across two languages and this is the only
|
||||
# check that holds this side to the bytes the other side emits.
|
||||
literal = ("# Trial log\n\n"
|
||||
"<!-- trial-log:begin variant=h2-scoped-problem-stress -->\n\n"
|
||||
"| n | game | after | round | step | state_hash | comment |\n"
|
||||
"|---|---|---|---|---|---|---|\n"
|
||||
"| 1 | 1 | 4 | 3 | Select | abc123def456 | it felt harder |\n"
|
||||
"\n<!-- trial-log:end -->\n")
|
||||
# Caught, because the failure mode is `parse` REFUSING the writer's
|
||||
# output — and an uncaught traceback names a line number where a
|
||||
# named check names the defect.
|
||||
try:
|
||||
literal_rows = parse(literal)
|
||||
except ValueError:
|
||||
literal_rows = []
|
||||
check("the bytes hotseat.rs writes are the bytes this parses",
|
||||
len(literal_rows) == 1
|
||||
and literal_rows[0]["variant"] == "h2-scoped-problem-stress",
|
||||
"hotseat.rs's the_trial_log_records_which_rules_were_played "
|
||||
"asserts this same string from the writing side")
|
||||
# And the marker as it was written before CB-WP-0046, also literal.
|
||||
try:
|
||||
older = parse(literal.replace(
|
||||
"<!-- trial-log:begin variant=h2-scoped-problem-stress -->",
|
||||
"<!-- trial-log:begin -->"))
|
||||
except ValueError:
|
||||
older = []
|
||||
check("a pre-CB-WP-0046 marker still opens a log", len(older) == 1,
|
||||
"ADR-0019 D4's discipline: old logs stay readable")
|
||||
|
||||
check("a legacy log can still be stamped",
|
||||
parse(f"# Trial log\n\n{BEGIN} variant=ground-darvo-r0 -->\n\n"
|
||||
"| n | round | step | state_hash | comment |\n|---|---|---|---|---|\n"
|
||||
"| 1 | 3 | Select | abc123def456 | x |\n"
|
||||
f"\n{END}\n")[0]["variant"] == "ground-darvo-r0")
|
||||
|
||||
# CB-WP-0032. A row of the wrong shape must be LOUD. This used to
|
||||
# `continue`, so a format change would have reported zero notes for
|
||||
# every log at once -- the failure the parser's own docstring names.
|
||||
|
|
|
|||
156
trials/2026-08-08-1903-2.yaml
Normal file
156
trials/2026-08-08-1903-2.yaml
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
scenario: ground/cb-play-session
|
||||
description: recorded by cb-play (CB-WP-0008 T02)
|
||||
covers: []
|
||||
provisional: false
|
||||
provisional_owner: ''
|
||||
provisional_raised: ''
|
||||
ruled: ''
|
||||
ruled_by: ''
|
||||
ruled_note: ''
|
||||
encodes_u_item: ''
|
||||
seed: 2
|
||||
setup:
|
||||
players: 3
|
||||
preset: standard-3p
|
||||
patch: {}
|
||||
commands:
|
||||
- actor: P1
|
||||
cmd: select_action
|
||||
args:
|
||||
action: SOLVE
|
||||
problem: 1
|
||||
- actor: P2
|
||||
cmd: select_action
|
||||
args:
|
||||
action: SOLVE
|
||||
problem: 1
|
||||
- actor: P3
|
||||
cmd: select_action
|
||||
args:
|
||||
action: SOLVE
|
||||
problem: 1
|
||||
- actor: SYSTEM
|
||||
cmd: reveal
|
||||
args: {}
|
||||
- actor: SYSTEM
|
||||
cmd: resolve
|
||||
args: {}
|
||||
- actor: SYSTEM
|
||||
cmd: end_round
|
||||
args: {}
|
||||
- actor: P1
|
||||
cmd: select_action
|
||||
args:
|
||||
action: INVESTIGATE
|
||||
problem: 2
|
||||
- actor: P2
|
||||
cmd: select_action
|
||||
args:
|
||||
action: INVESTIGATE
|
||||
problem: 2
|
||||
- actor: P3
|
||||
cmd: select_action
|
||||
args:
|
||||
action: INVESTIGATE
|
||||
problem: 2
|
||||
- actor: SYSTEM
|
||||
cmd: reveal
|
||||
args: {}
|
||||
- actor: SYSTEM
|
||||
cmd: resolve
|
||||
args: {}
|
||||
- actor: SYSTEM
|
||||
cmd: end_round
|
||||
args: {}
|
||||
- actor: P1
|
||||
cmd: select_action
|
||||
args:
|
||||
action: GROUND
|
||||
- actor: P2
|
||||
cmd: select_action
|
||||
args:
|
||||
action: GROUND
|
||||
- actor: P3
|
||||
cmd: select_action
|
||||
args:
|
||||
action: GROUND
|
||||
- actor: SYSTEM
|
||||
cmd: reveal
|
||||
args: {}
|
||||
- actor: P1
|
||||
cmd: choose_ground_mode
|
||||
args:
|
||||
mode: GR
|
||||
- actor: P2
|
||||
cmd: choose_ground_mode
|
||||
args:
|
||||
mode: GR
|
||||
- actor: P3
|
||||
cmd: choose_ground_mode
|
||||
args:
|
||||
mode: GR
|
||||
- actor: SYSTEM
|
||||
cmd: resolve
|
||||
args: {}
|
||||
- actor: SYSTEM
|
||||
cmd: end_round
|
||||
args: {}
|
||||
- actor: P1
|
||||
cmd: select_action
|
||||
args:
|
||||
action: SOLVE
|
||||
problem: 2
|
||||
- actor: P2
|
||||
cmd: select_action
|
||||
args:
|
||||
action: SOLVE
|
||||
problem: 2
|
||||
- actor: P3
|
||||
cmd: select_action
|
||||
args:
|
||||
action: SOLVE
|
||||
problem: 2
|
||||
- actor: SYSTEM
|
||||
cmd: reveal
|
||||
args: {}
|
||||
- actor: SYSTEM
|
||||
cmd: resolve
|
||||
args: {}
|
||||
- actor: SYSTEM
|
||||
cmd: end_round
|
||||
args: {}
|
||||
- actor: P1
|
||||
cmd: select_action
|
||||
args:
|
||||
action: INVESTIGATE
|
||||
problem: 3
|
||||
- actor: P2
|
||||
cmd: select_action
|
||||
args:
|
||||
action: GROUND
|
||||
- actor: P3
|
||||
cmd: select_action
|
||||
args:
|
||||
action: GROUND
|
||||
- actor: SYSTEM
|
||||
cmd: reveal
|
||||
args: {}
|
||||
- actor: P2
|
||||
cmd: choose_ground_mode
|
||||
args:
|
||||
mode: GR
|
||||
- actor: P3
|
||||
cmd: choose_ground_mode
|
||||
args:
|
||||
mode: GR
|
||||
- actor: SYSTEM
|
||||
cmd: resolve
|
||||
args: {}
|
||||
- actor: SYSTEM
|
||||
cmd: end_round
|
||||
args: {}
|
||||
expect:
|
||||
events: []
|
||||
state: {}
|
||||
rejects: []
|
||||
state_hash: 5c25b5357d90376d23caa08921a448e658c71c2936ab21e84374ad76525788b6
|
||||
179
trials/2026-08-08-1903-3.yaml
Normal file
179
trials/2026-08-08-1903-3.yaml
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
scenario: ground/cb-play-session
|
||||
description: recorded by cb-play (CB-WP-0008 T02)
|
||||
covers: []
|
||||
provisional: false
|
||||
provisional_owner: ''
|
||||
provisional_raised: ''
|
||||
ruled: ''
|
||||
ruled_by: ''
|
||||
ruled_note: ''
|
||||
encodes_u_item: ''
|
||||
seed: 3
|
||||
setup:
|
||||
players: 3
|
||||
preset: standard-3p
|
||||
patch: {}
|
||||
commands:
|
||||
- actor: P1
|
||||
cmd: select_action
|
||||
args:
|
||||
action: INVESTIGATE
|
||||
problem: 2
|
||||
- actor: P2
|
||||
cmd: select_action
|
||||
args:
|
||||
action: INVESTIGATE
|
||||
problem: 2
|
||||
- actor: P3
|
||||
cmd: select_action
|
||||
args:
|
||||
action: INVESTIGATE
|
||||
problem: 2
|
||||
- actor: SYSTEM
|
||||
cmd: reveal
|
||||
args: {}
|
||||
- actor: SYSTEM
|
||||
cmd: resolve
|
||||
args: {}
|
||||
- actor: SYSTEM
|
||||
cmd: end_round
|
||||
args: {}
|
||||
- actor: P1
|
||||
cmd: select_action
|
||||
args:
|
||||
action: GROUND
|
||||
- actor: P2
|
||||
cmd: select_action
|
||||
args:
|
||||
action: GROUND
|
||||
- actor: P3
|
||||
cmd: select_action
|
||||
args:
|
||||
action: GROUND
|
||||
- actor: SYSTEM
|
||||
cmd: reveal
|
||||
args: {}
|
||||
- actor: P1
|
||||
cmd: choose_ground_mode
|
||||
args:
|
||||
mode: GR
|
||||
- actor: P2
|
||||
cmd: choose_ground_mode
|
||||
args:
|
||||
mode: GR
|
||||
- actor: P3
|
||||
cmd: choose_ground_mode
|
||||
args:
|
||||
mode: GR
|
||||
- actor: SYSTEM
|
||||
cmd: resolve
|
||||
args: {}
|
||||
- actor: SYSTEM
|
||||
cmd: end_round
|
||||
args: {}
|
||||
- actor: P1
|
||||
cmd: select_action
|
||||
args:
|
||||
action: GROUND
|
||||
- actor: P2
|
||||
cmd: select_action
|
||||
args:
|
||||
action: GROUND
|
||||
- actor: P3
|
||||
cmd: select_action
|
||||
args:
|
||||
action: GROUND
|
||||
- actor: SYSTEM
|
||||
cmd: reveal
|
||||
args: {}
|
||||
- actor: P1
|
||||
cmd: choose_ground_mode
|
||||
args:
|
||||
mode: GR
|
||||
- actor: P2
|
||||
cmd: choose_ground_mode
|
||||
args:
|
||||
mode: GR
|
||||
- actor: P3
|
||||
cmd: choose_ground_mode
|
||||
args:
|
||||
mode: GR
|
||||
- actor: SYSTEM
|
||||
cmd: resolve
|
||||
args: {}
|
||||
- actor: SYSTEM
|
||||
cmd: end_round
|
||||
args: {}
|
||||
- actor: P1
|
||||
cmd: select_action
|
||||
args:
|
||||
action: GROUND
|
||||
- actor: P2
|
||||
cmd: select_action
|
||||
args:
|
||||
action: GROUND
|
||||
- actor: P3
|
||||
cmd: select_action
|
||||
args:
|
||||
action: GROUND
|
||||
- actor: SYSTEM
|
||||
cmd: reveal
|
||||
args: {}
|
||||
- actor: P1
|
||||
cmd: choose_ground_mode
|
||||
args:
|
||||
choice: reject_reverse
|
||||
mode: ND
|
||||
- actor: P2
|
||||
cmd: choose_ground_mode
|
||||
args:
|
||||
mode: GR
|
||||
- actor: P3
|
||||
cmd: choose_ground_mode
|
||||
args:
|
||||
mode: GR
|
||||
- actor: SYSTEM
|
||||
cmd: resolve
|
||||
args: {}
|
||||
- actor: SYSTEM
|
||||
cmd: end_round
|
||||
args: {}
|
||||
- actor: P1
|
||||
cmd: select_action
|
||||
args:
|
||||
action: ATTACK
|
||||
target: P3
|
||||
- actor: P2
|
||||
cmd: select_action
|
||||
args:
|
||||
action: GROUND
|
||||
- actor: P3
|
||||
cmd: select_action
|
||||
args:
|
||||
action: GROUND
|
||||
- actor: SYSTEM
|
||||
cmd: reveal
|
||||
args: {}
|
||||
- actor: P1
|
||||
cmd: choose_darvo_target
|
||||
args:
|
||||
problem: 1
|
||||
- actor: P2
|
||||
cmd: choose_ground_mode
|
||||
args:
|
||||
mode: GR
|
||||
- actor: P3
|
||||
cmd: choose_ground_mode
|
||||
args:
|
||||
mode: GR
|
||||
- actor: SYSTEM
|
||||
cmd: resolve
|
||||
args: {}
|
||||
- actor: SYSTEM
|
||||
cmd: end_round
|
||||
args: {}
|
||||
expect:
|
||||
events: []
|
||||
state: {}
|
||||
rejects: []
|
||||
state_hash: ae5b6a484c408dfb951ce82d7951a7f9b9c355e9b1cec394023b198154547744
|
||||
157
trials/2026-08-08-1903.yaml
Normal file
157
trials/2026-08-08-1903.yaml
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
scenario: ground/cb-play-session
|
||||
description: recorded by cb-play (CB-WP-0008 T02)
|
||||
covers: []
|
||||
provisional: false
|
||||
provisional_owner: ''
|
||||
provisional_raised: ''
|
||||
ruled: ''
|
||||
ruled_by: ''
|
||||
ruled_note: ''
|
||||
encodes_u_item: ''
|
||||
seed: 1
|
||||
setup:
|
||||
players: 3
|
||||
preset: standard-3p
|
||||
patch: {}
|
||||
commands:
|
||||
- actor: P1
|
||||
cmd: select_action
|
||||
args:
|
||||
action: INVESTIGATE
|
||||
problem: 2
|
||||
- actor: P2
|
||||
cmd: select_action
|
||||
args:
|
||||
action: SOLVE
|
||||
problem: 1
|
||||
- actor: P3
|
||||
cmd: select_action
|
||||
args:
|
||||
action: SOLVE
|
||||
problem: 1
|
||||
- actor: SYSTEM
|
||||
cmd: reveal
|
||||
args: {}
|
||||
- actor: SYSTEM
|
||||
cmd: resolve
|
||||
args: {}
|
||||
- actor: SYSTEM
|
||||
cmd: end_round
|
||||
args: {}
|
||||
- actor: P1
|
||||
cmd: select_action
|
||||
args:
|
||||
action: SUPPORT
|
||||
target: P2
|
||||
- actor: P2
|
||||
cmd: select_action
|
||||
args:
|
||||
action: INVESTIGATE
|
||||
problem: 3
|
||||
- actor: P3
|
||||
cmd: select_action
|
||||
args:
|
||||
action: INVESTIGATE
|
||||
problem: 3
|
||||
- actor: SYSTEM
|
||||
cmd: reveal
|
||||
args: {}
|
||||
- actor: P2
|
||||
cmd: respond_to_support
|
||||
args:
|
||||
response: accept_bond
|
||||
- actor: SYSTEM
|
||||
cmd: resolve
|
||||
args: {}
|
||||
- actor: SYSTEM
|
||||
cmd: end_round
|
||||
args: {}
|
||||
- actor: P1
|
||||
cmd: select_action
|
||||
args:
|
||||
action: GROUND
|
||||
- actor: P2
|
||||
cmd: select_action
|
||||
args:
|
||||
action: INVESTIGATE
|
||||
problem: 4
|
||||
- actor: P3
|
||||
cmd: select_action
|
||||
args:
|
||||
action: GROUND
|
||||
- actor: SYSTEM
|
||||
cmd: reveal
|
||||
args: {}
|
||||
- actor: P1
|
||||
cmd: choose_ground_mode
|
||||
args:
|
||||
mode: GR
|
||||
- actor: P3
|
||||
cmd: choose_ground_mode
|
||||
args:
|
||||
mode: GR
|
||||
- actor: SYSTEM
|
||||
cmd: resolve
|
||||
args: {}
|
||||
- actor: SYSTEM
|
||||
cmd: end_round
|
||||
args: {}
|
||||
- actor: P1
|
||||
cmd: select_action
|
||||
args:
|
||||
action: SOLVE
|
||||
problem: 3
|
||||
- actor: P2
|
||||
cmd: select_action
|
||||
args:
|
||||
action: GROUND
|
||||
- actor: P3
|
||||
cmd: select_action
|
||||
args:
|
||||
action: SOLVE
|
||||
problem: 2
|
||||
- actor: SYSTEM
|
||||
cmd: reveal
|
||||
args: {}
|
||||
- actor: P2
|
||||
cmd: choose_ground_mode
|
||||
args:
|
||||
mode: GR
|
||||
- actor: SYSTEM
|
||||
cmd: resolve
|
||||
args: {}
|
||||
- actor: SYSTEM
|
||||
cmd: end_round
|
||||
args: {}
|
||||
- actor: P1
|
||||
cmd: select_action
|
||||
args:
|
||||
action: SOLVE
|
||||
problem: 4
|
||||
- actor: P2
|
||||
cmd: select_action
|
||||
args:
|
||||
action: SUPPORT
|
||||
target: P1
|
||||
- actor: P3
|
||||
cmd: select_action
|
||||
args:
|
||||
action: GROUND
|
||||
- actor: SYSTEM
|
||||
cmd: reveal
|
||||
args: {}
|
||||
- actor: P3
|
||||
cmd: choose_ground_mode
|
||||
args:
|
||||
mode: GR
|
||||
- actor: SYSTEM
|
||||
cmd: resolve
|
||||
args: {}
|
||||
- actor: SYSTEM
|
||||
cmd: end_round
|
||||
args: {}
|
||||
expect:
|
||||
events: []
|
||||
state: {}
|
||||
rejects: []
|
||||
state_hash: 7ae42e026cd31344e4ef81b15d3cd7b6639bf9101689d99e8b50ef3614932ed8
|
||||
|
|
@ -3,6 +3,7 @@ id: CB-WP-0045
|
|||
kind: product
|
||||
title: "The table names what it shows"
|
||||
status: done
|
||||
state_hub_workstream_id: "4191bea7-f1ea-40b9-a957-600dabb298e2"
|
||||
---
|
||||
|
||||
# Purpose
|
||||
|
|
@ -66,6 +67,7 @@ modes at the point where the action card has already left the screen.
|
|||
id: CB-WP-0045-T01
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "bba3d8cb-7aa2-4bee-b97f-6ff8681dbec2"
|
||||
```
|
||||
|
||||
**Controls:**
|
||||
|
|
|
|||
128
workplans/CB-WP-0046-the-rule-the-placement-encodes.md
Normal file
128
workplans/CB-WP-0046-the-rule-the-placement-encodes.md
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
---
|
||||
id: CB-WP-0046
|
||||
kind: product
|
||||
title: "The rule the placement encodes"
|
||||
status: done
|
||||
---
|
||||
|
||||
# Purpose
|
||||
|
||||
```
|
||||
structural tier S (one new edition reader, one rendered passage, one
|
||||
marker attribute -- no rule, no dependency moved)
|
||||
declared tier S
|
||||
```
|
||||
|
||||
Closes the two gaps [CB-WP-0045](CB-WP-0045-the-table-names-what-it-shows.md)
|
||||
named as *not done*.
|
||||
|
||||
## The gap, and the wrong reason given for it
|
||||
|
||||
CB-WP-0045 recorded:
|
||||
|
||||
> *"Nothing explains what a scope does … it is a `ground-game` question
|
||||
> first: the scope rule is ours (Variant), not the edition's, so there is
|
||||
> no vendored sentence to render."*
|
||||
|
||||
**The premise was false.** `editions/experiments/h2-scoped-problem-stress/`
|
||||
ships `Rules_Text.csv`, and its `Problems / Stress scope` passage is the
|
||||
rule verbatim:
|
||||
|
||||
> *"Each Problem has stress_scope. global: every seat. personal: the
|
||||
> assigned owner only. bond: the owner and every seat connected to the
|
||||
> owner through one or more Bonds (the Bond network). If the owner has no
|
||||
> Bonds, bond scope behaves as personal. Rivalries do not expand bond
|
||||
> scope. Multiple unclaimed Problems stack (+1 each) for seats in their
|
||||
> scopes."*
|
||||
|
||||
**Nothing in clay-borg had ever read the file.** Twenty-two passages of
|
||||
player-facing rules, in the repo, unreachable by a player — a whole
|
||||
rulebook, where CB-WP-0045 found single columns.
|
||||
|
||||
**Third instance of F18's shape in two passes**, and the largest. The
|
||||
pattern is now specific enough to name: *when a package adds a file rather
|
||||
than a column, nothing notices.* `edition-check` verifies the digest and
|
||||
freshness of files it knows; a file no reader mentions is not stale, it is
|
||||
invisible. Recorded as a finding for `ground-game` rather than fixed here.
|
||||
|
||||
**The escalation is the part worth keeping.** CB-WP-0045 did not just
|
||||
leave a gap; it *explained* the gap with an argument that would have
|
||||
stopped the next reader from looking. A wrong reason for an open item is
|
||||
worse than an open item, because it retires the question.
|
||||
|
||||
## The log said nothing about which rules were played
|
||||
|
||||
`2026-08-08-1734`: *"Well, i did not notice any difference, maybe i did
|
||||
not start the H2 variation?"* **The log cannot answer that** — and neither
|
||||
could we, a week later, from the file.
|
||||
|
||||
The variant rides the **begin marker**, not an eighth column:
|
||||
|
||||
```
|
||||
<!-- trial-log:begin variant=h2-scoped-problem-stress -->
|
||||
```
|
||||
|
||||
It is a property of the **session** (fixed at startup, constant across
|
||||
`play again`), so a per-row column would repeat one fact on every line and
|
||||
force a third accepted row width on a parser whose docstring is about
|
||||
exactly that hazard (CB-WP-0032). The table shape is untouched.
|
||||
|
||||
**Stamped from `state.variant`, never from the `--variant` flag.** Three
|
||||
call sites once wrote `state.variant = v` directly instead of
|
||||
`with_variant()`, which leaves H2 inert; a log stamped from the flag would
|
||||
have recorded `h2` for a session that played the baseline. **A false
|
||||
provenance on real player words is worse than no provenance** — which is
|
||||
also why an unstamped log reports `unrecorded` and not `ground-darvo-r0`.
|
||||
|
||||
## Task: render the rule, stamp the log
|
||||
|
||||
```task
|
||||
id: CB-WP-0046-T01
|
||||
status: done
|
||||
priority: high
|
||||
```
|
||||
|
||||
**Controls, all mutation-proven:**
|
||||
|
||||
| mutation | what went red |
|
||||
|---|---|
|
||||
| the scope rule never renders | *"Problems are placed by scope and nothing says what a scope does"* |
|
||||
| it renders always, baseline included | *"the baseline table explains a scope distinction that is not in play"* |
|
||||
| our paraphrase instead of the passage | *"the scope explanation is not the edition's own words"* |
|
||||
| the writer drops the attribute | *"the log does not say which rules were played"* |
|
||||
| unstamped parses as `ground-darvo-r0` | *"an unstamped log says NOTHING, not 'baseline'"* |
|
||||
| `BEGIN` back to an exact match | *"the bytes hotseat.rs writes are the bytes this parses"* |
|
||||
|
||||
**The last mutation is the finding.** It went green the first time. Every
|
||||
`trials.py` fixture composed its marker out of `BEGIN`, so all of them
|
||||
**followed `BEGIN` wherever it went** — including away from what
|
||||
`hotseat.rs` actually writes. Nine checks about the marker, and reverting
|
||||
the marker broke every real stamped log with all nine still green.
|
||||
|
||||
The format is shared across two languages, so the control has to be a
|
||||
**literal**: the exact bytes the writer emits, asserted from the reading
|
||||
side, with the writer's own test asserting the same string from the other.
|
||||
A fixture built from the constant under test cannot test the constant.
|
||||
|
||||
`stress_scope_rule()` matches on the **heading**, not on row 20 — a row
|
||||
number is a property of today's file, a heading is a property of the rule
|
||||
— and returns `None` if the edition stops carrying it (ADR-0018: an absent
|
||||
explanation is reported absent, never replaced by ours).
|
||||
|
||||
**Done 2026-08-08.** `make all` green; `cargo test`: 80 render + 35 play;
|
||||
24 `trials.py` checks. Verified on a live `make ground VARIANT=h2` page.
|
||||
|
||||
## Not done here
|
||||
|
||||
- **The other 21 passages are still unread.** `h2_rules_text()` returns
|
||||
the whole rulebook and one caller uses one row of it. The Round-step
|
||||
passages in particular are the account of the sequence the player is
|
||||
standing in, and there is nowhere on the page that says it.
|
||||
- **The baseline has no `Rules_Text.csv` at all**, so this explains only
|
||||
the experiment. A baseline player still gets no rulebook — the reverse
|
||||
of the usual gap, and a `ground-game` question.
|
||||
- **The 11 existing notes stay `unrecorded` forever.** Their variant was
|
||||
never written down; inventing one is the defect this pass exists to
|
||||
avoid. `2026-08-08-1818`'s three notes are almost certainly H2 on the
|
||||
internal evidence of what they say, and *almost certainly* is not a
|
||||
provenance.
|
||||
Loading…
Add table
Add a link
Reference in a new issue