CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit
Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat
play, at a measured marginal AM-4a cost of zero.
games-ground shipped: 23 third-party crates
cb-render-html: 23 third-party crates
new crates introduced: 0
Measured, not asserted — the survey's own lesson. AM-4a is unmoved at
246,250; own source is 7,636 -> 9,652.
What shipped:
crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship
graph), input.rs (pointer facts -> commands),
serve.rs (Guard, Request, loopback bind)
tools/cb-play hotseat.rs + `--serve PORT`
Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null.
The renderer targets the existing Project trait; the port waits for
stage 2's wgpu implementation to be its second use.
The six controls, all live, all mutation-checked (8 mutations, each red
for its stated reason):
1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind
4 a token-less request is refused, in the unit AND over a real socket
5 JS may not construct commands — the page reports pointer facts, Rust
resolves them against the legal list the aggregate already offered,
and a test asserts the emitted script contains no game vocabulary
6 the coverage gate crosses the language boundary: it walks the
serialized view for leaf paths and requires each token to appear in
the PARSED emitted document, with a test that the parse really is a
parse (script/style contents must not count as rendered)
The gate fired on its author again, on its first run: ground_choices.*.
choice, ground_choices.*.problem and players.*.blame_from were in neither
list. The last is the one worth keeping — an EMPTY vector is a leaf path
of its own, and it now renders as an explicit absence.
Also, a mutation that did not go red: removing the Sec-Fetch-Site arm
alone left the cross-site test green, because the Origin check caught it
independently. Both had to be removed before the control bit. Recorded
because a control that passes for a reason you did not intend has not
been demonstrated.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
883b608f36
commit
84d688688d
14 changed files with 2058 additions and 2 deletions
|
|
@ -32,6 +32,9 @@ pub struct Config {
|
|||
/// Where to write the finished game as a scenario file. A session
|
||||
/// somebody played becomes a regression test.
|
||||
pub record: Option<std::path::PathBuf>,
|
||||
/// Serve human seats in a browser instead of on the terminal
|
||||
/// (ADR-0007). `Some(0)` lets the OS pick the port.
|
||||
pub serve: Option<u16>,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
|
|
@ -43,6 +46,7 @@ impl Default for Config {
|
|||
bot: "greedy".into(),
|
||||
replay_dir: None,
|
||||
record: None,
|
||||
serve: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -213,10 +217,28 @@ fn run_game<'a, R: BufRead + 'a, W: Write + 'a>(
|
|||
// Human seats and bot seats fill one policy vector; the driver does
|
||||
// not know which is which, which is the point.
|
||||
let failure: Failure = Default::default();
|
||||
// ADR-0007: a browser seat and a CLI seat are both just a Policy, so
|
||||
// the driver cannot tell them apart — which is the property that lets
|
||||
// a browser game replay as a scenario like any other.
|
||||
let server = match config.serve {
|
||||
Some(port) => {
|
||||
let s = std::rc::Rc::new(crate::hotseat::Server::bind(port)?);
|
||||
let _ = writeln!(out.borrow_mut(), " open {}", s.url());
|
||||
let _ = out.borrow_mut().flush();
|
||||
Some(s)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let mut policies: Vec<Box<dyn Policy + 'a>> = Vec::new();
|
||||
for seat in 0..config.players {
|
||||
if config.human_seats.contains(&seat) {
|
||||
policies.push(Box::new(HumanPolicy::new(input, out, failure.clone())));
|
||||
match &server {
|
||||
Some(s) => policies.push(Box::new(crate::hotseat::SeatPolicy::new(
|
||||
s.clone(),
|
||||
failure.clone(),
|
||||
))),
|
||||
None => policies.push(Box::new(HumanPolicy::new(input, out, failure.clone()))),
|
||||
}
|
||||
} else {
|
||||
policies.push(bot_policy(&config.bot, config.seed + u64::from(seat))?);
|
||||
}
|
||||
|
|
@ -248,6 +270,20 @@ fn run_game<'a, R: BufRead + 'a, W: Write + 'a>(
|
|||
game.commands,
|
||||
&end_hash[..12]
|
||||
);
|
||||
// ADR-0007 control 1 leaves evidence rather than only a 403: a
|
||||
// session that was probed says so, so a player finds out from the
|
||||
// transcript rather than from nothing at all.
|
||||
if let Some(s) = &server {
|
||||
let refusals = s.refusals();
|
||||
if refusals.is_empty() {
|
||||
let _ = writeln!(w, " no requests were refused this session");
|
||||
} else {
|
||||
let _ = writeln!(w, " {} request(s) refused:", refusals.len());
|
||||
for r in &refusals {
|
||||
let _ = writeln!(w, " {r}");
|
||||
}
|
||||
}
|
||||
}
|
||||
let _ = w.flush();
|
||||
drop(w);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue