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>
2026-08-02 04:27:25 +02:00
|
|
|
//! The emitted document: HTML shell, inline SVG table, inline JavaScript.
|
|
|
|
|
//!
|
|
|
|
|
//! ADR-0007 Decision 1. Marginal AM-4a cost zero — this is string
|
|
|
|
|
//! formatting, and the browser draws it.
|
|
|
|
|
//!
|
|
|
|
|
//! ## What the JavaScript is allowed to be
|
|
|
|
|
//!
|
|
|
|
|
//! ADR-0007 Decision 5 bars it from constructing commands. [`SCRIPT`] is
|
|
|
|
|
//! therefore the whole of it, it is a constant, and it does one thing:
|
|
|
|
|
//! record which element the pointer went down on, which it came up on, and
|
|
|
|
|
//! POST the pair. It contains no game vocabulary — no action names, no
|
|
|
|
|
//! seats, no rules. If a rule ever needs to appear in it, the decision is
|
|
|
|
|
//! wrong and ADR-0007 says to revisit it rather than widen the control.
|
|
|
|
|
|
|
|
|
|
use std::fmt::Write as _;
|
|
|
|
|
|
|
|
|
|
use cb_kernel::PlayerId;
|
|
|
|
|
use games_ground::view::{GroundView, PlayerView, ProblemView, SelectionView};
|
|
|
|
|
|
|
|
|
|
use crate::input::action_id;
|
|
|
|
|
|
|
|
|
|
/// Escape for text and attribute contexts alike.
|
|
|
|
|
///
|
|
|
|
|
/// Everything interpolated into the document goes through this. Card
|
|
|
|
|
/// suits and seat numbers cannot currently carry a `<`, but "cannot
|
|
|
|
|
/// currently" is how injection bugs are written.
|
|
|
|
|
pub fn esc(s: &str) -> String {
|
|
|
|
|
let mut out = String::with_capacity(s.len());
|
|
|
|
|
for c in s.chars() {
|
|
|
|
|
match c {
|
|
|
|
|
'&' => out.push_str("&"),
|
|
|
|
|
'<' => out.push_str("<"),
|
|
|
|
|
'>' => out.push_str(">"),
|
|
|
|
|
'"' => out.push_str("""),
|
|
|
|
|
'\'' => out.push_str("'"),
|
|
|
|
|
_ => out.push(c),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
out
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn seat_name(p: PlayerId) -> String {
|
|
|
|
|
format!("P{}", p.0 + 1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn cards(list: &[games_ground::SolutionCard]) -> String {
|
|
|
|
|
if list.is_empty() {
|
|
|
|
|
return "none".to_string();
|
|
|
|
|
}
|
|
|
|
|
list.iter()
|
|
|
|
|
.map(|c| format!("{:?}", c.suit))
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
.join(" ")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The only JavaScript in the project. See the module docs.
|
|
|
|
|
pub const SCRIPT: &str = r#"
|
|
|
|
|
(function () {
|
|
|
|
|
var down = null;
|
CB-WP-0016: the drop target that was never there
Provenance (tier S, one paragraph in lieu of survey and ADR): the human
check that kept INTENT stage 1 open was run and the drag was broken.
Root cause, worth more than the instance: drop targets were ids, and an
id must be unique, so exactly one element could ever be seat-0. The
relationship-graph circle took it and the seat card that every action
card's own text points at -- 'drag Attack onto a seat' -- silently had
none. A seat is drawn twice and both drawings are the seat; the document
model could not express that.
Drop keys are now data-drop. Any number of elements may carry the same
key, so a seat is droppable on its card and on its graph node. Measured
on a live server: seat-0/1/2 each appear twice, id survives only on
cb-status which is the one element the script looks up, and
down=action-attack&up=seat-1 returns ok.
Second defect: a drop on nothing returned without posting and without
touching the status line, so a broken target was indistinguishable from
a working page. resolve already refuses rather than defaulting, which is
right; refusing SILENTLY is not. The page now reports the raw fact --
'took action-attack, let go over nothing droppable' -- which names
elements, not moves, so ADR-0007 control 5 holds.
And the honest part: the general check added here -- every offered
affordance names a key that exists, driven through Policy::choose over
four real bot games -- does NOT catch the reported defect. seat-0 did
exist, on the graph circle. It is kept because a wholly absent target is
a real class, and paired with a targeted regression test that does catch
it. Three mutations, each red for its stated reason, including the
reported defect reintroduced; only the targeted test fires on that one.
A cb-play assertion matched id="action-ground" as a substring while
describing itself as checking the page; rewritten through drop_keys.
make all exits 0. Stage 1 stays open: verified by tests, mutation and a
live server, not by a human dragging.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 20:48:18 +02:00
|
|
|
function key(e) {
|
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>
2026-08-02 04:27:25 +02:00
|
|
|
var n = e.target;
|
CB-WP-0016: the drop target that was never there
Provenance (tier S, one paragraph in lieu of survey and ADR): the human
check that kept INTENT stage 1 open was run and the drag was broken.
Root cause, worth more than the instance: drop targets were ids, and an
id must be unique, so exactly one element could ever be seat-0. The
relationship-graph circle took it and the seat card that every action
card's own text points at -- 'drag Attack onto a seat' -- silently had
none. A seat is drawn twice and both drawings are the seat; the document
model could not express that.
Drop keys are now data-drop. Any number of elements may carry the same
key, so a seat is droppable on its card and on its graph node. Measured
on a live server: seat-0/1/2 each appear twice, id survives only on
cb-status which is the one element the script looks up, and
down=action-attack&up=seat-1 returns ok.
Second defect: a drop on nothing returned without posting and without
touching the status line, so a broken target was indistinguishable from
a working page. resolve already refuses rather than defaulting, which is
right; refusing SILENTLY is not. The page now reports the raw fact --
'took action-attack, let go over nothing droppable' -- which names
elements, not moves, so ADR-0007 control 5 holds.
And the honest part: the general check added here -- every offered
affordance names a key that exists, driven through Policy::choose over
four real bot games -- does NOT catch the reported defect. seat-0 did
exist, on the graph circle. It is kept because a wholly absent target is
a real class, and paired with a targeted regression test that does catch
it. Three mutations, each red for its stated reason, including the
reported defect reintroduced; only the targeted test fires on that one.
A cb-play assertion matched id="action-ground" as a substring while
describing itself as checking the page; rewritten through drop_keys.
make all exits 0. Stage 1 stays open: verified by tests, mutation and a
live server, not by a human dragging.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 20:48:18 +02:00
|
|
|
while (n && !(n.getAttribute && n.getAttribute('data-drop'))) { n = n.parentNode; }
|
|
|
|
|
return n ? n.getAttribute('data-drop') : null;
|
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>
2026-08-02 04:27:25 +02:00
|
|
|
}
|
CB-WP-0016: the drop target that was never there
Provenance (tier S, one paragraph in lieu of survey and ADR): the human
check that kept INTENT stage 1 open was run and the drag was broken.
Root cause, worth more than the instance: drop targets were ids, and an
id must be unique, so exactly one element could ever be seat-0. The
relationship-graph circle took it and the seat card that every action
card's own text points at -- 'drag Attack onto a seat' -- silently had
none. A seat is drawn twice and both drawings are the seat; the document
model could not express that.
Drop keys are now data-drop. Any number of elements may carry the same
key, so a seat is droppable on its card and on its graph node. Measured
on a live server: seat-0/1/2 each appear twice, id survives only on
cb-status which is the one element the script looks up, and
down=action-attack&up=seat-1 returns ok.
Second defect: a drop on nothing returned without posting and without
touching the status line, so a broken target was indistinguishable from
a working page. resolve already refuses rather than defaulting, which is
right; refusing SILENTLY is not. The page now reports the raw fact --
'took action-attack, let go over nothing droppable' -- which names
elements, not moves, so ADR-0007 control 5 holds.
And the honest part: the general check added here -- every offered
affordance names a key that exists, driven through Policy::choose over
four real bot games -- does NOT catch the reported defect. seat-0 did
exist, on the graph circle. It is kept because a wholly absent target is
a real class, and paired with a targeted regression test that does catch
it. Three mutations, each red for its stated reason, including the
reported defect reintroduced; only the targeted test fires on that one.
A cb-play assertion matched id="action-ground" as a substring while
describing itself as checking the page; rewritten through drop_keys.
make all exits 0. Stage 1 stays open: verified by tests, mutation and a
live server, not by a human dragging.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 20:48:18 +02:00
|
|
|
document.addEventListener('pointerdown', function (e) { down = key(e); });
|
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>
2026-08-02 04:27:25 +02:00
|
|
|
document.addEventListener('pointerup', function (e) {
|
CB-WP-0016: the drop target that was never there
Provenance (tier S, one paragraph in lieu of survey and ADR): the human
check that kept INTENT stage 1 open was run and the drag was broken.
Root cause, worth more than the instance: drop targets were ids, and an
id must be unique, so exactly one element could ever be seat-0. The
relationship-graph circle took it and the seat card that every action
card's own text points at -- 'drag Attack onto a seat' -- silently had
none. A seat is drawn twice and both drawings are the seat; the document
model could not express that.
Drop keys are now data-drop. Any number of elements may carry the same
key, so a seat is droppable on its card and on its graph node. Measured
on a live server: seat-0/1/2 each appear twice, id survives only on
cb-status which is the one element the script looks up, and
down=action-attack&up=seat-1 returns ok.
Second defect: a drop on nothing returned without posting and without
touching the status line, so a broken target was indistinguishable from
a working page. resolve already refuses rather than defaulting, which is
right; refusing SILENTLY is not. The page now reports the raw fact --
'took action-attack, let go over nothing droppable' -- which names
elements, not moves, so ADR-0007 control 5 holds.
And the honest part: the general check added here -- every offered
affordance names a key that exists, driven through Policy::choose over
four real bot games -- does NOT catch the reported defect. seat-0 did
exist, on the graph circle. It is kept because a wholly absent target is
a real class, and paired with a targeted regression test that does catch
it. Three mutations, each red for its stated reason, including the
reported defect reintroduced; only the targeted test fires on that one.
A cb-play assertion matched id="action-ground" as a substring while
describing itself as checking the page; rewritten through drop_keys.
make all exits 0. Stage 1 stays open: verified by tests, mutation and a
live server, not by a human dragging.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 20:48:18 +02:00
|
|
|
var up = key(e);
|
|
|
|
|
if (!down || !up) {
|
|
|
|
|
// CB-WP-0016 T02: refusing is right; refusing SILENTLY is what let a
|
|
|
|
|
// broken drop target survive a human sitting in front of it. Report
|
|
|
|
|
// the raw fact — which element the pointer took and where it let go.
|
|
|
|
|
// This decides nothing: it names elements, not moves.
|
|
|
|
|
document.getElementById('cb-status').textContent =
|
|
|
|
|
down ? 'took ' + down + ', let go over nothing droppable'
|
|
|
|
|
: 'nothing droppable under the pointer';
|
|
|
|
|
down = null;
|
|
|
|
|
return;
|
|
|
|
|
}
|
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>
2026-08-02 04:27:25 +02:00
|
|
|
var body = 'down=' + encodeURIComponent(down) + '&up=' + encodeURIComponent(up);
|
|
|
|
|
down = null;
|
|
|
|
|
fetch(window.CB_ENDPOINT, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
|
|
|
body: body
|
|
|
|
|
}).then(function (r) { return r.text(); }).then(function (t) {
|
|
|
|
|
document.getElementById('cb-status').textContent = t;
|
|
|
|
|
if (t.indexOf('ok') === 0) { window.location.reload(); }
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
})();
|
|
|
|
|
"#;
|
|
|
|
|
|
|
|
|
|
const STYLE: &str = "
|
|
|
|
|
body{font:14px/1.5 ui-monospace,monospace;margin:1.5rem;background:#12141a;color:#dde}
|
|
|
|
|
h1,h2{font-size:1rem;margin:1.2rem 0 .4rem;color:#9cf}
|
|
|
|
|
.row{display:flex;flex-wrap:wrap;gap:.5rem;align-items:flex-start}
|
|
|
|
|
.card{border:1px solid #445;border-radius:6px;padding:.5rem .7rem;background:#1b1e26}
|
|
|
|
|
.card[data-viewer=true]{border-color:#9cf}
|
|
|
|
|
.act{cursor:grab;user-select:none;background:#243;border-color:#5a7}
|
|
|
|
|
.btn{cursor:pointer;background:#332a3a;border-color:#a7d}
|
|
|
|
|
.k{color:#89a}
|
|
|
|
|
#cb-status{margin-top:1rem;color:#fc9;min-height:1.2em}
|
|
|
|
|
svg{background:#1b1e26;border:1px solid #445;border-radius:6px}
|
|
|
|
|
";
|
|
|
|
|
|
|
|
|
|
fn problem_svg(out: &mut String, priority: u32, p: &ProblemView, x: i32) {
|
|
|
|
|
let (label, sub, fill) = match p {
|
|
|
|
|
ProblemView::FaceDown => ("face down".to_string(), String::new(), "#2a2f3a"),
|
|
|
|
|
ProblemView::FaceUp {
|
|
|
|
|
suit,
|
|
|
|
|
value,
|
|
|
|
|
denied,
|
|
|
|
|
claimed_by,
|
|
|
|
|
protected_this_round,
|
|
|
|
|
} => {
|
|
|
|
|
let mut sub = String::new();
|
|
|
|
|
if *denied {
|
|
|
|
|
sub.push_str("denied ");
|
|
|
|
|
}
|
|
|
|
|
if *protected_this_round {
|
|
|
|
|
sub.push_str("protected ");
|
|
|
|
|
}
|
|
|
|
|
if let Some(c) = claimed_by {
|
|
|
|
|
let _ = write!(sub, "claimed by {}", seat_name(*c));
|
|
|
|
|
}
|
|
|
|
|
(
|
|
|
|
|
format!("{suit:?} {value}"),
|
|
|
|
|
sub.trim_end().to_string(),
|
|
|
|
|
"#26303a",
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
let _ = write!(
|
|
|
|
|
out,
|
CB-WP-0016: the drop target that was never there
Provenance (tier S, one paragraph in lieu of survey and ADR): the human
check that kept INTENT stage 1 open was run and the drag was broken.
Root cause, worth more than the instance: drop targets were ids, and an
id must be unique, so exactly one element could ever be seat-0. The
relationship-graph circle took it and the seat card that every action
card's own text points at -- 'drag Attack onto a seat' -- silently had
none. A seat is drawn twice and both drawings are the seat; the document
model could not express that.
Drop keys are now data-drop. Any number of elements may carry the same
key, so a seat is droppable on its card and on its graph node. Measured
on a live server: seat-0/1/2 each appear twice, id survives only on
cb-status which is the one element the script looks up, and
down=action-attack&up=seat-1 returns ok.
Second defect: a drop on nothing returned without posting and without
touching the status line, so a broken target was indistinguishable from
a working page. resolve already refuses rather than defaulting, which is
right; refusing SILENTLY is not. The page now reports the raw fact --
'took action-attack, let go over nothing droppable' -- which names
elements, not moves, so ADR-0007 control 5 holds.
And the honest part: the general check added here -- every offered
affordance names a key that exists, driven through Policy::choose over
four real bot games -- does NOT catch the reported defect. seat-0 did
exist, on the graph circle. It is kept because a wholly absent target is
a real class, and paired with a targeted regression test that does catch
it. Three mutations, each red for its stated reason, including the
reported defect reintroduced; only the targeted test fires on that one.
A cb-play assertion matched id="action-ground" as a substring while
describing itself as checking the page; rewritten through drop_keys.
make all exits 0. Stage 1 stays open: verified by tests, mutation and a
live server, not by a human dragging.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 20:48:18 +02:00
|
|
|
"<g data-drop=\"problem-{priority}\"><rect x=\"{x}\" y=\"10\" width=\"120\" height=\"78\" rx=\"8\" \
|
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>
2026-08-02 04:27:25 +02:00
|
|
|
fill=\"{fill}\" stroke=\"#5a6b7a\"/>\
|
|
|
|
|
<text x=\"{tx}\" y=\"36\" fill=\"#dde\" font-size=\"13\">{label}</text>\
|
|
|
|
|
<text x=\"{tx}\" y=\"56\" fill=\"#89a\" font-size=\"11\">priority {priority}</text>\
|
|
|
|
|
<text x=\"{tx}\" y=\"74\" fill=\"#fc9\" font-size=\"11\">{sub}</text></g>",
|
|
|
|
|
x = x,
|
|
|
|
|
tx = x + 10,
|
|
|
|
|
label = esc(&label),
|
|
|
|
|
sub = esc(&sub),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The relationship graph — the one element every Rust 2D toolkit would
|
|
|
|
|
/// have left us to hand-roll, and the reason SVG earns its place here
|
|
|
|
|
/// rather than merely fitting the budget (CB-RES-0006 §5).
|
|
|
|
|
fn relations_svg(view: &GroundView) -> String {
|
|
|
|
|
let n = view.players.len().max(1);
|
|
|
|
|
let (cx, cy, r) = (200.0f64, 150.0f64, 110.0f64);
|
|
|
|
|
let pos: Vec<(PlayerId, f64, f64)> = view
|
|
|
|
|
.players
|
|
|
|
|
.keys()
|
|
|
|
|
.enumerate()
|
|
|
|
|
.map(|(i, p)| {
|
|
|
|
|
let a = std::f64::consts::TAU * (i as f64) / (n as f64) - std::f64::consts::FRAC_PI_2;
|
|
|
|
|
(*p, cx + r * a.cos(), cy + r * a.sin())
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
let find = |p: PlayerId| {
|
|
|
|
|
pos.iter()
|
|
|
|
|
.find(|(q, _, _)| *q == p)
|
|
|
|
|
.map(|(_, x, y)| (*x, *y))
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let mut s = String::from(
|
|
|
|
|
"<svg width=\"400\" height=\"300\" role=\"img\" aria-label=\"relationship graph\">",
|
|
|
|
|
);
|
|
|
|
|
for (pair, rel) in &view.relations {
|
|
|
|
|
if let (Some((x1, y1)), Some((x2, y2))) = (find(pair.0), find(pair.1)) {
|
|
|
|
|
let colour = match rel {
|
|
|
|
|
games_ground::Relation::Bond => "#5c9",
|
|
|
|
|
games_ground::Relation::Rivalry => "#c66",
|
|
|
|
|
};
|
|
|
|
|
let _ = write!(
|
|
|
|
|
s,
|
|
|
|
|
"<line x1=\"{x1:.0}\" y1=\"{y1:.0}\" x2=\"{x2:.0}\" y2=\"{y2:.0}\" \
|
|
|
|
|
stroke=\"{colour}\" stroke-width=\"2\"/>\
|
|
|
|
|
<text x=\"{mx:.0}\" y=\"{my:.0}\" fill=\"{colour}\" font-size=\"10\">{rel:?}</text>",
|
|
|
|
|
mx = (x1 + x2) / 2.0,
|
|
|
|
|
my = (y1 + y2) / 2.0 - 3.0,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for (p, x, y) in &pos {
|
|
|
|
|
let is_viewer = view.viewer == Some(*p);
|
|
|
|
|
let focus = view
|
|
|
|
|
.focus
|
|
|
|
|
.get(p)
|
|
|
|
|
.map(|f| format!(" \u{2192}{}", seat_name(*f)));
|
|
|
|
|
let _ = write!(
|
|
|
|
|
s,
|
CB-WP-0016: the drop target that was never there
Provenance (tier S, one paragraph in lieu of survey and ADR): the human
check that kept INTENT stage 1 open was run and the drag was broken.
Root cause, worth more than the instance: drop targets were ids, and an
id must be unique, so exactly one element could ever be seat-0. The
relationship-graph circle took it and the seat card that every action
card's own text points at -- 'drag Attack onto a seat' -- silently had
none. A seat is drawn twice and both drawings are the seat; the document
model could not express that.
Drop keys are now data-drop. Any number of elements may carry the same
key, so a seat is droppable on its card and on its graph node. Measured
on a live server: seat-0/1/2 each appear twice, id survives only on
cb-status which is the one element the script looks up, and
down=action-attack&up=seat-1 returns ok.
Second defect: a drop on nothing returned without posting and without
touching the status line, so a broken target was indistinguishable from
a working page. resolve already refuses rather than defaulting, which is
right; refusing SILENTLY is not. The page now reports the raw fact --
'took action-attack, let go over nothing droppable' -- which names
elements, not moves, so ADR-0007 control 5 holds.
And the honest part: the general check added here -- every offered
affordance names a key that exists, driven through Policy::choose over
four real bot games -- does NOT catch the reported defect. seat-0 did
exist, on the graph circle. It is kept because a wholly absent target is
a real class, and paired with a targeted regression test that does catch
it. Three mutations, each red for its stated reason, including the
reported defect reintroduced; only the targeted test fires on that one.
A cb-play assertion matched id="action-ground" as a substring while
describing itself as checking the page; rewritten through drop_keys.
make all exits 0. Stage 1 stays open: verified by tests, mutation and a
live server, not by a human dragging.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 20:48:18 +02:00
|
|
|
"<g data-drop=\"seat-{raw}\"><circle cx=\"{x:.0}\" cy=\"{y:.0}\" r=\"26\" fill=\"#1b1e26\" \
|
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>
2026-08-02 04:27:25 +02:00
|
|
|
stroke=\"{stroke}\" stroke-width=\"2\"/>\
|
|
|
|
|
<text x=\"{x:.0}\" y=\"{ty:.0}\" fill=\"#dde\" font-size=\"12\" \
|
|
|
|
|
text-anchor=\"middle\">{name}</text>\
|
|
|
|
|
<text x=\"{x:.0}\" y=\"{ty2:.0}\" fill=\"#89a\" font-size=\"10\" \
|
|
|
|
|
text-anchor=\"middle\">{focus}</text></g>",
|
|
|
|
|
raw = p.0,
|
|
|
|
|
stroke = if is_viewer { "#9cf" } else { "#5a6b7a" },
|
|
|
|
|
ty = y + 2.0,
|
|
|
|
|
ty2 = y + 16.0,
|
|
|
|
|
name = seat_name(*p),
|
|
|
|
|
focus = esc(focus.as_deref().unwrap_or("")),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
s.push_str("</svg>");
|
|
|
|
|
s
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn player_card(out: &mut String, id: PlayerId, p: &PlayerView, view: &GroundView) {
|
|
|
|
|
let is_viewer = view.viewer == Some(id);
|
|
|
|
|
let _ = write!(
|
|
|
|
|
out,
|
CB-WP-0016: the drop target that was never there
Provenance (tier S, one paragraph in lieu of survey and ADR): the human
check that kept INTENT stage 1 open was run and the drag was broken.
Root cause, worth more than the instance: drop targets were ids, and an
id must be unique, so exactly one element could ever be seat-0. The
relationship-graph circle took it and the seat card that every action
card's own text points at -- 'drag Attack onto a seat' -- silently had
none. A seat is drawn twice and both drawings are the seat; the document
model could not express that.
Drop keys are now data-drop. Any number of elements may carry the same
key, so a seat is droppable on its card and on its graph node. Measured
on a live server: seat-0/1/2 each appear twice, id survives only on
cb-status which is the one element the script looks up, and
down=action-attack&up=seat-1 returns ok.
Second defect: a drop on nothing returned without posting and without
touching the status line, so a broken target was indistinguishable from
a working page. resolve already refuses rather than defaulting, which is
right; refusing SILENTLY is not. The page now reports the raw fact --
'took action-attack, let go over nothing droppable' -- which names
elements, not moves, so ADR-0007 control 5 holds.
And the honest part: the general check added here -- every offered
affordance names a key that exists, driven through Policy::choose over
four real bot games -- does NOT catch the reported defect. seat-0 did
exist, on the graph circle. It is kept because a wholly absent target is
a real class, and paired with a targeted regression test that does catch
it. Three mutations, each red for its stated reason, including the
reported defect reintroduced; only the targeted test fires on that one.
A cb-play assertion matched id="action-ground" as a substring while
describing itself as checking the page; rewritten through drop_keys.
make all exits 0. Stage 1 stays open: verified by tests, mutation and a
live server, not by a human dragging.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 20:48:18 +02:00
|
|
|
// CB-WP-0016 T01: the seat card is a drop target. It could not be
|
|
|
|
|
// while drop keys were `id`s — the graph node had already taken
|
|
|
|
|
// `seat-{n}` and ids must be unique, so the card the instruction
|
|
|
|
|
// text points at silently had none.
|
|
|
|
|
"<div class=\"card\" data-viewer=\"{is_viewer}\" data-drop=\"seat-{raw}\">\
|
|
|
|
|
<b>{name}</b>{you}<br>",
|
|
|
|
|
raw = id.0,
|
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>
2026-08-02 04:27:25 +02:00
|
|
|
name = seat_name(id),
|
|
|
|
|
you = if is_viewer { " (you)" } else { "" },
|
|
|
|
|
);
|
|
|
|
|
let _ = write!(
|
|
|
|
|
out,
|
|
|
|
|
"<span class=\"k\">stress</span> {} <span class=\"k\">protect</span> {} \
|
|
|
|
|
<span class=\"k\">darvo</span> {:?}<br>",
|
|
|
|
|
p.stress, p.protection, p.darvo
|
|
|
|
|
);
|
|
|
|
|
let _ = write!(
|
|
|
|
|
out,
|
CB-WP-0016: the drop target that was never there
Provenance (tier S, one paragraph in lieu of survey and ADR): the human
check that kept INTENT stage 1 open was run and the drag was broken.
Root cause, worth more than the instance: drop targets were ids, and an
id must be unique, so exactly one element could ever be seat-0. The
relationship-graph circle took it and the seat card that every action
card's own text points at -- 'drag Attack onto a seat' -- silently had
none. A seat is drawn twice and both drawings are the seat; the document
model could not express that.
Drop keys are now data-drop. Any number of elements may carry the same
key, so a seat is droppable on its card and on its graph node. Measured
on a live server: seat-0/1/2 each appear twice, id survives only on
cb-status which is the one element the script looks up, and
down=action-attack&up=seat-1 returns ok.
Second defect: a drop on nothing returned without posting and without
touching the status line, so a broken target was indistinguishable from
a working page. resolve already refuses rather than defaulting, which is
right; refusing SILENTLY is not. The page now reports the raw fact --
'took action-attack, let go over nothing droppable' -- which names
elements, not moves, so ADR-0007 control 5 holds.
And the honest part: the general check added here -- every offered
affordance names a key that exists, driven through Policy::choose over
four real bot games -- does NOT catch the reported defect. seat-0 did
exist, on the graph circle. It is kept because a wholly absent target is
a real class, and paired with a targeted regression test that does catch
it. Three mutations, each red for its stated reason, including the
reported defect reintroduced; only the targeted test fires on that one.
A cb-play assertion matched id="action-ground" as a substring while
describing itself as checking the page; rewritten through drop_keys.
make all exits 0. Stage 1 stays open: verified by tests, mutation and a
live server, not by a human dragging.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 20:48:18 +02:00
|
|
|
"<span data-drop=\"freedom-{raw}\" class=\"act\"><span class=\"k\">freedom</span> \
|
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>
2026-08-02 04:27:25 +02:00
|
|
|
{ready}{lifted}</span><br>",
|
|
|
|
|
raw = id.0,
|
|
|
|
|
ready = if p.freedom_ready { "READY" } else { "spent" },
|
|
|
|
|
lifted = if p.freedom_gate_lifted {
|
|
|
|
|
" gate lifted"
|
|
|
|
|
} else {
|
|
|
|
|
""
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
let blame = if p.blame_from.is_empty() {
|
|
|
|
|
"none".to_string()
|
|
|
|
|
} else {
|
|
|
|
|
p.blame_from
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|b| seat_name(*b))
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
.join(" ")
|
|
|
|
|
};
|
|
|
|
|
let _ = write!(
|
|
|
|
|
out,
|
|
|
|
|
"<span class=\"k\">blamed by</span> {}<br>",
|
|
|
|
|
esc(&blame)
|
|
|
|
|
);
|
|
|
|
|
match &p.hand {
|
|
|
|
|
Some(h) => {
|
|
|
|
|
let _ = write!(
|
|
|
|
|
out,
|
|
|
|
|
"<span class=\"k\">hand</span> {} ({} cards)<br>",
|
|
|
|
|
esc(&cards(h)),
|
|
|
|
|
p.hand_size
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
None => {
|
|
|
|
|
let _ = write!(
|
|
|
|
|
out,
|
|
|
|
|
"<span class=\"k\">hand</span> {} card(s), hidden<br>",
|
|
|
|
|
p.hand_size
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if let Some(sel) = view.selections.get(&id) {
|
|
|
|
|
let _ = write!(
|
|
|
|
|
out,
|
|
|
|
|
"<span class=\"k\">selected</span> {}<br>",
|
|
|
|
|
match sel {
|
|
|
|
|
SelectionView::Hidden => "face down".to_string(),
|
|
|
|
|
SelectionView::Shown(s) => esc(&format!("{s:?}")),
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if let Some(m) = view.ground_modes.get(&id) {
|
|
|
|
|
let _ = write!(out, "<span class=\"k\">ground mode</span> {m:?}<br>");
|
|
|
|
|
}
|
|
|
|
|
if let Some(c) = view.ground_choices.get(&id) {
|
|
|
|
|
let _ = write!(out, "<span class=\"k\">ground choice</span> {c:?}<br>");
|
|
|
|
|
}
|
|
|
|
|
if let Some(r) = view.support_responses.get(&id) {
|
|
|
|
|
let _ = write!(out, "<span class=\"k\">support</span> {r:?}<br>");
|
|
|
|
|
}
|
|
|
|
|
if let Some(t) = view.darvo_targets.get(&id) {
|
|
|
|
|
let _ = write!(out, "<span class=\"k\">darvo target</span> {t:?}<br>");
|
|
|
|
|
}
|
|
|
|
|
out.push_str("</div>");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Render the whole table as a standalone document.
|
|
|
|
|
///
|
|
|
|
|
/// `may_pass` adds the one affordance that is not a command: declining to
|
|
|
|
|
/// act where the driver allows it. Like every other element it carries an
|
|
|
|
|
/// id and nothing else — the page still reports only that the pointer went
|
|
|
|
|
/// down and up on `pass`.
|
|
|
|
|
///
|
|
|
|
|
/// `legal` is the list the aggregate offered; the buttons carry indices
|
|
|
|
|
/// into it and nothing else (ADR-0007 control 5). `endpoint` carries the
|
|
|
|
|
/// per-process token (control 1) — the page cannot mint one.
|
|
|
|
|
pub fn document(
|
|
|
|
|
view: &GroundView,
|
|
|
|
|
legal: &[games_ground::GroundCommand],
|
|
|
|
|
endpoint: &str,
|
|
|
|
|
seat: Option<PlayerId>,
|
|
|
|
|
may_pass: bool,
|
|
|
|
|
) -> String {
|
|
|
|
|
let mut s = String::with_capacity(8192);
|
|
|
|
|
let _ = write!(
|
|
|
|
|
s,
|
|
|
|
|
"<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\">\
|
|
|
|
|
<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\
|
|
|
|
|
<title>GROUND \u{2014} round {round}</title><style>{STYLE}</style></head><body>",
|
|
|
|
|
round = view.round
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let _ = write!(
|
|
|
|
|
s,
|
|
|
|
|
"<h1>GROUND \u{2014} round {round}, step {step:?}</h1>\
|
|
|
|
|
<div><span class=\"k\">lead</span> {lead} \
|
|
|
|
|
<span class=\"k\">scoring</span> {mode:?} \
|
|
|
|
|
<span class=\"k\">viewing as</span> {who}</div>",
|
|
|
|
|
round = view.round,
|
|
|
|
|
step = view.step,
|
|
|
|
|
lead = seat_name(view.lead),
|
|
|
|
|
mode = view.mode,
|
|
|
|
|
who = match view.viewer {
|
|
|
|
|
Some(p) => format!("{} (their hand only)", seat_name(p)),
|
|
|
|
|
None => "a spectator (no hands)".to_string(),
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
s.push_str(
|
|
|
|
|
"<h2>problems</h2><svg width=\"760\" height=\"100\" role=\"img\" aria-label=\"problems\">",
|
|
|
|
|
);
|
|
|
|
|
if view.problems.is_empty() {
|
|
|
|
|
s.push_str(
|
|
|
|
|
"<text x=\"12\" y=\"52\" fill=\"#89a\" font-size=\"12\">no problems in play</text>",
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
for (i, (priority, p)) in view.problems.iter().enumerate() {
|
|
|
|
|
problem_svg(&mut s, *priority, p, 10 + (i as i32) * 130);
|
|
|
|
|
}
|
|
|
|
|
s.push_str("</svg>");
|
|
|
|
|
|
|
|
|
|
s.push_str("<h2>relationships</h2>");
|
|
|
|
|
s.push_str(&relations_svg(view));
|
|
|
|
|
|
|
|
|
|
s.push_str("<h2>seats</h2><div class=\"row\">");
|
|
|
|
|
for (id, p) in &view.players {
|
|
|
|
|
player_card(&mut s, *id, p, view);
|
|
|
|
|
}
|
|
|
|
|
s.push_str("</div>");
|
|
|
|
|
|
|
|
|
|
let _ = write!(
|
|
|
|
|
s,
|
|
|
|
|
"<h2>solutions</h2><div><span class=\"k\">deck</span> {} remaining \
|
|
|
|
|
<span class=\"k\">discard</span> {}</div>",
|
|
|
|
|
view.solution_deck_len,
|
|
|
|
|
esc(&cards(&view.solution_discard)),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if let Some(o) = &view.outcome {
|
|
|
|
|
let personal = o
|
|
|
|
|
.personal
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|(p, v)| format!("{} {v:+}", seat_name(*p)))
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
.join(" ");
|
|
|
|
|
let winners = if o.winners.is_empty() {
|
|
|
|
|
"nobody".to_string()
|
|
|
|
|
} else {
|
|
|
|
|
o.winners
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|w| seat_name(*w))
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
.join(" ")
|
|
|
|
|
};
|
|
|
|
|
let coalitions = if o.coalitions.is_empty() {
|
|
|
|
|
"none".to_string()
|
|
|
|
|
} else {
|
|
|
|
|
o.coalitions
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|c| format!("{c:?}"))
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
.join(" ")
|
|
|
|
|
};
|
|
|
|
|
let _ = write!(
|
|
|
|
|
s,
|
|
|
|
|
"<h2>outcome</h2><div class=\"card\">\
|
|
|
|
|
<span class=\"k\">total</span> {total} of {threshold} \
|
|
|
|
|
<span class=\"k\">group</span> {group}<br>\
|
|
|
|
|
<span class=\"k\">personal</span> {personal}<br>\
|
|
|
|
|
<span class=\"k\">coalitions</span> {coalitions}<br>\
|
|
|
|
|
<span class=\"k\">mastery</span> {mastery}<br>\
|
|
|
|
|
<span class=\"k\">winners</span> {winners}</div>",
|
|
|
|
|
total = o.total,
|
|
|
|
|
threshold = o.threshold,
|
|
|
|
|
group = if o.group_success {
|
|
|
|
|
"success"
|
|
|
|
|
} else {
|
|
|
|
|
"failure"
|
|
|
|
|
},
|
|
|
|
|
personal = esc(&personal),
|
|
|
|
|
coalitions = esc(&coalitions),
|
|
|
|
|
mastery = match o.mastery {
|
|
|
|
|
Some(m) => format!("{m:+}"),
|
|
|
|
|
None => "none".to_string(),
|
|
|
|
|
},
|
|
|
|
|
winners = esc(&winners),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !legal.is_empty() {
|
|
|
|
|
s.push_str("<h2>your move</h2><div class=\"row\">");
|
|
|
|
|
for a in [
|
|
|
|
|
games_ground::Action::Investigate,
|
|
|
|
|
games_ground::Action::Solve,
|
|
|
|
|
games_ground::Action::Support,
|
|
|
|
|
games_ground::Action::Attack,
|
|
|
|
|
games_ground::Action::Ground,
|
|
|
|
|
] {
|
|
|
|
|
let offered = seat.is_some_and(|seat| {
|
|
|
|
|
legal.iter().any(|c| {
|
|
|
|
|
crate::input::affordance(c, seat).is_some_and(|(f, _)| f == action_id(a))
|
|
|
|
|
})
|
|
|
|
|
});
|
|
|
|
|
if offered {
|
|
|
|
|
let _ = write!(
|
|
|
|
|
s,
|
CB-WP-0016: the drop target that was never there
Provenance (tier S, one paragraph in lieu of survey and ADR): the human
check that kept INTENT stage 1 open was run and the drag was broken.
Root cause, worth more than the instance: drop targets were ids, and an
id must be unique, so exactly one element could ever be seat-0. The
relationship-graph circle took it and the seat card that every action
card's own text points at -- 'drag Attack onto a seat' -- silently had
none. A seat is drawn twice and both drawings are the seat; the document
model could not express that.
Drop keys are now data-drop. Any number of elements may carry the same
key, so a seat is droppable on its card and on its graph node. Measured
on a live server: seat-0/1/2 each appear twice, id survives only on
cb-status which is the one element the script looks up, and
down=action-attack&up=seat-1 returns ok.
Second defect: a drop on nothing returned without posting and without
touching the status line, so a broken target was indistinguishable from
a working page. resolve already refuses rather than defaulting, which is
right; refusing SILENTLY is not. The page now reports the raw fact --
'took action-attack, let go over nothing droppable' -- which names
elements, not moves, so ADR-0007 control 5 holds.
And the honest part: the general check added here -- every offered
affordance names a key that exists, driven through Policy::choose over
four real bot games -- does NOT catch the reported defect. seat-0 did
exist, on the graph circle. It is kept because a wholly absent target is
a real class, and paired with a targeted regression test that does catch
it. Three mutations, each red for its stated reason, including the
reported defect reintroduced; only the targeted test fires on that one.
A cb-play assertion matched id="action-ground" as a substring while
describing itself as checking the page; rewritten through drop_keys.
make all exits 0. Stage 1 stays open: verified by tests, mutation and a
live server, not by a human dragging.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 20:48:18 +02:00
|
|
|
"<div class=\"card act\" data-drop=\"{id}\">drag {a:?} onto a seat, a problem, \
|
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>
2026-08-02 04:27:25 +02:00
|
|
|
or the table</div>",
|
|
|
|
|
id = action_id(a),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
s.push_str("</div><div class=\"row\">");
|
|
|
|
|
for (i, c) in legal.iter().enumerate() {
|
|
|
|
|
let spatial = seat.is_some_and(|seat| crate::input::affordance(c, seat).is_some());
|
|
|
|
|
if !spatial {
|
|
|
|
|
let _ = write!(
|
|
|
|
|
s,
|
CB-WP-0016: the drop target that was never there
Provenance (tier S, one paragraph in lieu of survey and ADR): the human
check that kept INTENT stage 1 open was run and the drag was broken.
Root cause, worth more than the instance: drop targets were ids, and an
id must be unique, so exactly one element could ever be seat-0. The
relationship-graph circle took it and the seat card that every action
card's own text points at -- 'drag Attack onto a seat' -- silently had
none. A seat is drawn twice and both drawings are the seat; the document
model could not express that.
Drop keys are now data-drop. Any number of elements may carry the same
key, so a seat is droppable on its card and on its graph node. Measured
on a live server: seat-0/1/2 each appear twice, id survives only on
cb-status which is the one element the script looks up, and
down=action-attack&up=seat-1 returns ok.
Second defect: a drop on nothing returned without posting and without
touching the status line, so a broken target was indistinguishable from
a working page. resolve already refuses rather than defaulting, which is
right; refusing SILENTLY is not. The page now reports the raw fact --
'took action-attack, let go over nothing droppable' -- which names
elements, not moves, so ADR-0007 control 5 holds.
And the honest part: the general check added here -- every offered
affordance names a key that exists, driven through Policy::choose over
four real bot games -- does NOT catch the reported defect. seat-0 did
exist, on the graph circle. It is kept because a wholly absent target is
a real class, and paired with a targeted regression test that does catch
it. Three mutations, each red for its stated reason, including the
reported defect reintroduced; only the targeted test fires on that one.
A cb-play assertion matched id="action-ground" as a substring while
describing itself as checking the page; rewritten through drop_keys.
make all exits 0. Stage 1 stays open: verified by tests, mutation and a
live server, not by a human dragging.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 20:48:18 +02:00
|
|
|
"<div class=\"card btn\" data-drop=\"cmd-{i}\">{}</div>",
|
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>
2026-08-02 04:27:25 +02:00
|
|
|
esc(&format!("{c:?}"))
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
s.push_str(
|
CB-WP-0016: the drop target that was never there
Provenance (tier S, one paragraph in lieu of survey and ADR): the human
check that kept INTENT stage 1 open was run and the drag was broken.
Root cause, worth more than the instance: drop targets were ids, and an
id must be unique, so exactly one element could ever be seat-0. The
relationship-graph circle took it and the seat card that every action
card's own text points at -- 'drag Attack onto a seat' -- silently had
none. A seat is drawn twice and both drawings are the seat; the document
model could not express that.
Drop keys are now data-drop. Any number of elements may carry the same
key, so a seat is droppable on its card and on its graph node. Measured
on a live server: seat-0/1/2 each appear twice, id survives only on
cb-status which is the one element the script looks up, and
down=action-attack&up=seat-1 returns ok.
Second defect: a drop on nothing returned without posting and without
touching the status line, so a broken target was indistinguishable from
a working page. resolve already refuses rather than defaulting, which is
right; refusing SILENTLY is not. The page now reports the raw fact --
'took action-attack, let go over nothing droppable' -- which names
elements, not moves, so ADR-0007 control 5 holds.
And the honest part: the general check added here -- every offered
affordance names a key that exists, driven through Policy::choose over
four real bot games -- does NOT catch the reported defect. seat-0 did
exist, on the graph circle. It is kept because a wholly absent target is
a real class, and paired with a targeted regression test that does catch
it. Three mutations, each red for its stated reason, including the
reported defect reintroduced; only the targeted test fires on that one.
A cb-play assertion matched id="action-ground" as a substring while
describing itself as checking the page; rewritten through drop_keys.
make all exits 0. Stage 1 stays open: verified by tests, mutation and a
live server, not by a human dragging.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 20:48:18 +02:00
|
|
|
"</div><div class=\"card\" data-drop=\"table\">the table \u{2014} drop here for an \
|
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>
2026-08-02 04:27:25 +02:00
|
|
|
untargeted action</div>",
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if may_pass {
|
CB-WP-0016: the drop target that was never there
Provenance (tier S, one paragraph in lieu of survey and ADR): the human
check that kept INTENT stage 1 open was run and the drag was broken.
Root cause, worth more than the instance: drop targets were ids, and an
id must be unique, so exactly one element could ever be seat-0. The
relationship-graph circle took it and the seat card that every action
card's own text points at -- 'drag Attack onto a seat' -- silently had
none. A seat is drawn twice and both drawings are the seat; the document
model could not express that.
Drop keys are now data-drop. Any number of elements may carry the same
key, so a seat is droppable on its card and on its graph node. Measured
on a live server: seat-0/1/2 each appear twice, id survives only on
cb-status which is the one element the script looks up, and
down=action-attack&up=seat-1 returns ok.
Second defect: a drop on nothing returned without posting and without
touching the status line, so a broken target was indistinguishable from
a working page. resolve already refuses rather than defaulting, which is
right; refusing SILENTLY is not. The page now reports the raw fact --
'took action-attack, let go over nothing droppable' -- which names
elements, not moves, so ADR-0007 control 5 holds.
And the honest part: the general check added here -- every offered
affordance names a key that exists, driven through Policy::choose over
four real bot games -- does NOT catch the reported defect. seat-0 did
exist, on the graph circle. It is kept because a wholly absent target is
a real class, and paired with a targeted regression test that does catch
it. Three mutations, each red for its stated reason, including the
reported defect reintroduced; only the targeted test fires on that one.
A cb-play assertion matched id="action-ground" as a substring while
describing itself as checking the page; rewritten through drop_keys.
make all exits 0. Stage 1 stays open: verified by tests, mutation and a
live server, not by a human dragging.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 20:48:18 +02:00
|
|
|
s.push_str("<div class=\"card btn\" data-drop=\"pass\">pass \u{2014} decline to act</div>");
|
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>
2026-08-02 04:27:25 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let _ = write!(
|
|
|
|
|
s,
|
|
|
|
|
"<div id=\"cb-status\">ready</div>\
|
|
|
|
|
<script>window.CB_ENDPOINT={};</script><script>{SCRIPT}</script></body></html>",
|
|
|
|
|
json_string(endpoint),
|
|
|
|
|
);
|
|
|
|
|
s
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Quote a string as a JSON literal, so a token can never end the script.
|
|
|
|
|
fn json_string(s: &str) -> String {
|
|
|
|
|
let mut out = String::with_capacity(s.len() + 2);
|
|
|
|
|
out.push('"');
|
|
|
|
|
for c in s.chars() {
|
|
|
|
|
match c {
|
|
|
|
|
'"' => out.push_str("\\\""),
|
|
|
|
|
'\\' => out.push_str("\\\\"),
|
|
|
|
|
'<' => out.push_str("\\u003c"),
|
|
|
|
|
'>' => out.push_str("\\u003e"),
|
|
|
|
|
'&' => out.push_str("\\u0026"),
|
|
|
|
|
c if (c as u32) < 0x20 => {
|
|
|
|
|
let _ = write!(out, "\\u{:04x}", c as u32);
|
|
|
|
|
}
|
|
|
|
|
c => out.push(c),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
out.push('"');
|
|
|
|
|
out
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Extract the document's visible text and element ids.
|
|
|
|
|
///
|
|
|
|
|
/// ADR-0007 control 6 requires the coverage gate to assert over the
|
|
|
|
|
/// **parsed emitted document**, not over the Rust that emits it. This is
|
|
|
|
|
/// that parse: it drops markup and returns what a reader would see, plus
|
|
|
|
|
/// the ids a pointer can address. A substring search over the raw source
|
|
|
|
|
/// would happily find a token inside a comment or a style rule.
|
CB-WP-0016: the drop target that was never there
Provenance (tier S, one paragraph in lieu of survey and ADR): the human
check that kept INTENT stage 1 open was run and the drag was broken.
Root cause, worth more than the instance: drop targets were ids, and an
id must be unique, so exactly one element could ever be seat-0. The
relationship-graph circle took it and the seat card that every action
card's own text points at -- 'drag Attack onto a seat' -- silently had
none. A seat is drawn twice and both drawings are the seat; the document
model could not express that.
Drop keys are now data-drop. Any number of elements may carry the same
key, so a seat is droppable on its card and on its graph node. Measured
on a live server: seat-0/1/2 each appear twice, id survives only on
cb-status which is the one element the script looks up, and
down=action-attack&up=seat-1 returns ok.
Second defect: a drop on nothing returned without posting and without
touching the status line, so a broken target was indistinguishable from
a working page. resolve already refuses rather than defaulting, which is
right; refusing SILENTLY is not. The page now reports the raw fact --
'took action-attack, let go over nothing droppable' -- which names
elements, not moves, so ADR-0007 control 5 holds.
And the honest part: the general check added here -- every offered
affordance names a key that exists, driven through Policy::choose over
four real bot games -- does NOT catch the reported defect. seat-0 did
exist, on the graph circle. It is kept because a wholly absent target is
a real class, and paired with a targeted regression test that does catch
it. Three mutations, each red for its stated reason, including the
reported defect reintroduced; only the targeted test fires on that one.
A cb-play assertion matched id="action-ground" as a substring while
describing itself as checking the page; rewritten through drop_keys.
make all exits 0. Stage 1 stays open: verified by tests, mutation and a
live server, not by a human dragging.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 20:48:18 +02:00
|
|
|
/// Every `data-drop="…"` value in the document.
|
|
|
|
|
///
|
|
|
|
|
/// CB-WP-0016. A real parse of the attribute rather than a substring
|
|
|
|
|
/// search: `html.contains("seat-1")` would be satisfied by the *text*
|
|
|
|
|
/// "seat-1" and by `data-drop="seat-10"`, and the point of the check this
|
|
|
|
|
/// feeds is that an affordance can name a target that is not there.
|
|
|
|
|
///
|
|
|
|
|
/// **Drop keys are `data-drop`, not `id`, and that is the fix for
|
|
|
|
|
/// CB-WP-0016.** An `id` must be unique in a document, so exactly one
|
|
|
|
|
/// element could ever be `seat-0` — the relationship-graph circle took it
|
|
|
|
|
/// and the seat card the instruction text points at went without. A seat
|
|
|
|
|
/// is drawn twice and both drawings are the seat.
|
|
|
|
|
pub fn drop_keys(html: &str) -> std::collections::BTreeSet<String> {
|
|
|
|
|
let mut out = std::collections::BTreeSet::new();
|
|
|
|
|
let mut rest = html;
|
|
|
|
|
while let Some(i) = rest.find("data-drop=\"") {
|
|
|
|
|
let after = &rest[i + 11..];
|
|
|
|
|
match after.find('"') {
|
|
|
|
|
Some(j) => {
|
|
|
|
|
out.insert(after[..j].to_string());
|
|
|
|
|
rest = &after[j..];
|
|
|
|
|
}
|
|
|
|
|
None => break,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
out
|
|
|
|
|
}
|
|
|
|
|
|
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>
2026-08-02 04:27:25 +02:00
|
|
|
pub fn text_of(html: &str) -> String {
|
|
|
|
|
let mut out = String::with_capacity(html.len() / 2);
|
|
|
|
|
let bytes: Vec<char> = html.chars().collect();
|
|
|
|
|
let mut i = 0;
|
|
|
|
|
let mut skip_to: Option<&str> = None;
|
|
|
|
|
while i < bytes.len() {
|
|
|
|
|
if bytes[i] == '<' {
|
|
|
|
|
// Find the tag name.
|
|
|
|
|
let start = i + 1;
|
|
|
|
|
let mut j = start;
|
|
|
|
|
while j < bytes.len() && bytes[j] != '>' {
|
|
|
|
|
j += 1;
|
|
|
|
|
}
|
|
|
|
|
let tag: String = bytes[start..j.min(bytes.len())].iter().collect();
|
|
|
|
|
let lower = tag.to_ascii_lowercase();
|
|
|
|
|
let name = lower
|
|
|
|
|
.trim_start_matches('/')
|
|
|
|
|
.split([' ', '\t', '\n', '>'])
|
|
|
|
|
.next()
|
|
|
|
|
.unwrap_or("")
|
|
|
|
|
.to_string();
|
|
|
|
|
|
|
|
|
|
if let Some(want) = skip_to {
|
|
|
|
|
if lower.starts_with('/') && name == want {
|
|
|
|
|
skip_to = None;
|
|
|
|
|
}
|
|
|
|
|
} else if name == "script" || name == "style" {
|
|
|
|
|
// Their contents are not text a reader sees.
|
|
|
|
|
if !lower.starts_with('/') && !lower.ends_with('/') {
|
|
|
|
|
skip_to = Some(if name == "script" { "script" } else { "style" });
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Ids are addressable surface, so they count as rendered.
|
|
|
|
|
if let Some(k) = lower.find("id=\"") {
|
|
|
|
|
let rest = &tag[k + 4..];
|
|
|
|
|
if let Some(end) = rest.find('"') {
|
|
|
|
|
out.push(' ');
|
|
|
|
|
out.push_str(&rest[..end]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
i = j + 1;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if skip_to.is_none() {
|
|
|
|
|
out.push(bytes[i]);
|
|
|
|
|
}
|
|
|
|
|
i += 1;
|
|
|
|
|
}
|
|
|
|
|
// Unescape the entities esc() introduced, so a test looks for the text
|
|
|
|
|
// a reader sees rather than its encoding.
|
|
|
|
|
out.replace("<", "<")
|
|
|
|
|
.replace(">", ">")
|
|
|
|
|
.replace(""", "\"")
|
|
|
|
|
.replace("'", "'")
|
|
|
|
|
.replace("&", "&")
|
|
|
|
|
}
|