CB-WP-0027 T01-T04: the commentary track
The meta view beside the table, and a note channel that provably cannot carry a move. T01 (ADR-0014). ADR-0007 D5 is SCOPED, NOT AMENDED, and the reason it was easy is that PointerFact::parse already refuses any unrecognised field -- a comment could not reach the command path even by accident. So /command carries pointer facts, /note carries text, and Note has no code path to GroundCommand. Comments live in trials/<date>-<slug>.md, not in ScenarioFile: a scenario is executed, replayed and hashed, and prose in it is data the runner must ignore, which is how a format rots. The state hash binds; round and step are for reading. And the retention question, decided before any comment was written: RAW NOTES NEVER LEAVE clay-borg. A note reaches ground-game only by being promoted to a register finding, by a human, with the wording chosen then -- "the DARVO sequence is infuriating" is useful signal and a bad way to open a message to the game's designer. T02. CSS grid, minmax(0,1fr) on both tracks -- load-bearing, because a grid child defaults to min-content width and without it the SVG table refuses to shrink and pushes the meta column off-screen, looking correct on the developer's monitor and broken everywhere else. Single-column fallback under 64rem. The running tally moved into the panel so it is visible WHILE PLAYING; it only appeared on the ending page before, and a score you see once the game is over informs nothing. T03. A plain <form method="post">, so the box works with the script disabled; the command channel needs JavaScript because a drag is not a form submission, a comment is one. 303 See Other so a reload does not re-post. esc()'s first hostile input: <script>alert(1)</script> renders escaped AND STILL READABLE -- escaping that eats the player's words is its own defect. Verified over real HTTP: note posted 303, hostile note stored as text, empty note refused 400, game did not advance. T04. tools/trials.py and make trials. THE REPORT'S DESIGN CHANGED BECAUSE I RAN IT: the first version called any note without a recording an orphan, so a live session reported every note as broken -- the recording is only written at game end. A metric that cries wolf is one nobody reads, which is the exact failure this pass exists to prevent. Now ok / pending / orphan, and only orphan is a target-0 number. The self-test exercises the REPORTING path, not just the parser, because design-baseline.py had a green self-test and an unexercised reporting path and that is where it rotted. And a latent Makefile defect surfaced: make trials did nothing, because trials is also a directory and Make saw an up-to-date file. design, difficulty and trials -- added by CB-WP-0022, CB-WP-0025 and this pass -- were ALL missing from .PHONY; only the one that collided revealed it. make all: exit 0. 49 render tests, 26 cb-play, loop-lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
5c6e322d5f
commit
4fb506fcd1
11 changed files with 1002 additions and 10 deletions
|
|
@ -255,6 +255,25 @@ h1,h2{font-size:1rem;margin:1.2rem 0 .4rem;color:#9cf}
|
|||
styling alone would leave a dead control that still looks alive to
|
||||
anything reading the DOM. */
|
||||
.sealed{opacity:.3;cursor:default;pointer-events:none;filter:grayscale(1)}
|
||||
/* CB-WP-0027 T02. `minmax(0,...)` on both tracks, because a grid child
|
||||
defaults to min-content width and the SVG table would refuse to shrink,
|
||||
pushing the meta column off-screen instead of narrowing.
|
||||
The single-column fallback is deliberate rather than incidental: the
|
||||
page was responsive by accident before this. */
|
||||
.cb-cols{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,24rem);
|
||||
gap:1.2rem;align-items:start}
|
||||
.cb-game{min-width:0}
|
||||
.cb-meta{min-width:0;border-left:1px solid #2a3140;padding-left:1.1rem}
|
||||
#cb-note{display:flex;flex-direction:column;gap:.5rem}
|
||||
#cb-note textarea{width:100%;box-sizing:border-box;font:inherit;color:#dde;
|
||||
background:#12141a;border:1px solid #445;border-radius:5px;padding:.5rem}
|
||||
#cb-note button{align-self:flex-start;font:inherit;cursor:pointer;color:#dde;
|
||||
background:#332a3a;border:1px solid #a7d;border-radius:5px;padding:.35rem .8rem}
|
||||
.note{border-left:2px solid #a7d;padding-left:.6rem;margin:.4rem 0;white-space:pre-wrap}
|
||||
@media (max-width:64rem){
|
||||
.cb-cols{grid-template-columns:minmax(0,1fr)}
|
||||
.cb-meta{border-left:none;border-top:1px solid #2a3140;padding-left:0;padding-top:1rem}
|
||||
}
|
||||
/* CB-WP-0024 T03: the card a seat played, in that seat's area. */
|
||||
.played{display:block;margin:.3rem 0}
|
||||
.k{color:#89a}
|
||||
|
|
@ -679,7 +698,7 @@ pub fn document(
|
|||
seat: Option<PlayerId>,
|
||||
may_pass: bool,
|
||||
) -> String {
|
||||
document_with_log(view, legal, endpoint, seat, may_pass, &[])
|
||||
document_with_log(view, legal, endpoint, seat, may_pass, &[], &[])
|
||||
}
|
||||
|
||||
/// The table, plus the game log (CB-WP-0018 T02).
|
||||
|
|
@ -690,6 +709,7 @@ pub fn document_with_log(
|
|||
seat: Option<PlayerId>,
|
||||
may_pass: bool,
|
||||
log: &[LogLine],
|
||||
meta: &[String],
|
||||
) -> String {
|
||||
let mut s = String::with_capacity(8192);
|
||||
let _ = write!(
|
||||
|
|
@ -716,9 +736,16 @@ pub fn document_with_log(
|
|||
},
|
||||
);
|
||||
|
||||
// CB-WP-0027 T02: the table on the left, everything *about* the table
|
||||
// on the right. The log moves right because it is commentary on the
|
||||
// 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);
|
||||
s.push_str("</div><div class=\"cb-meta\">");
|
||||
meta_section(&mut s, meta);
|
||||
log_section(&mut s, log);
|
||||
s.push_str("</div></div>");
|
||||
let _ = write!(
|
||||
s,
|
||||
"<div id=\"cb-status\">ready</div>\
|
||||
|
|
@ -729,6 +756,40 @@ pub fn document_with_log(
|
|||
s
|
||||
}
|
||||
|
||||
/// The meta panel's own content: whatever the caller wants a player to see
|
||||
/// *about* the session rather than about the position.
|
||||
///
|
||||
/// Empty is a legitimate state — a first game has no tally and may have no
|
||||
/// notes — and renders as nothing rather than as an empty heading.
|
||||
fn meta_section(s: &mut String, meta: &[String]) {
|
||||
// CB-WP-0027 T03: the comment box. Always present — the panel's
|
||||
// purpose is that a player can say something at any moment, and a box
|
||||
// that appears only sometimes trains them not to look for it.
|
||||
//
|
||||
// A plain form POSTing to /note, so it works with the script disabled.
|
||||
// The command channel needs JavaScript because a drag is not a form
|
||||
// submission; a comment is, and making it depend on the script would
|
||||
// add a failure mode for no gain.
|
||||
s.push_str(
|
||||
"<h2>what are you thinking?</h2>\
|
||||
<form class=\"card\" method=\"post\" action=\"/note\" id=\"cb-note\">\
|
||||
<textarea name=\"note\" rows=\"4\" placeholder=\"why this move, what is \
|
||||
unclear, what is annoying \u{2014} bound to this position\"></textarea>\
|
||||
<button type=\"submit\">note it</button></form>",
|
||||
);
|
||||
if meta.is_empty() {
|
||||
return;
|
||||
}
|
||||
s.push_str("<h2>this session</h2><div class=\"card\">");
|
||||
for (i, line) in meta.iter().enumerate() {
|
||||
if i > 0 {
|
||||
s.push_str("<br>");
|
||||
}
|
||||
s.push_str(&esc(line));
|
||||
}
|
||||
s.push_str("</div>");
|
||||
}
|
||||
|
||||
/// The table itself: problems, relationships, seats, solutions, outcome.
|
||||
///
|
||||
/// Factored out of [`document`] so [`ending`] shows the SAME table rather
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue