CB-WP-0016: the drop target that was never there
Some checks failed
ci / check (push) Failing after 4s
Some checks failed
ci / check (push) Failing after 4s
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>
This commit is contained in:
parent
4df2d0a4ae
commit
bf72a1863a
9 changed files with 496 additions and 27 deletions
|
|
@ -57,15 +57,25 @@ fn cards(list: &[games_ground::SolutionCard]) -> String {
|
|||
pub const SCRIPT: &str = r#"
|
||||
(function () {
|
||||
var down = null;
|
||||
function id(e) {
|
||||
function key(e) {
|
||||
var n = e.target;
|
||||
while (n && !n.id) { n = n.parentNode; }
|
||||
return n ? n.id : null;
|
||||
while (n && !(n.getAttribute && n.getAttribute('data-drop'))) { n = n.parentNode; }
|
||||
return n ? n.getAttribute('data-drop') : null;
|
||||
}
|
||||
document.addEventListener('pointerdown', function (e) { down = id(e); });
|
||||
document.addEventListener('pointerdown', function (e) { down = key(e); });
|
||||
document.addEventListener('pointerup', function (e) {
|
||||
var up = id(e);
|
||||
if (!down || !up) { down = null; return; }
|
||||
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;
|
||||
}
|
||||
var body = 'down=' + encodeURIComponent(down) + '&up=' + encodeURIComponent(up);
|
||||
down = null;
|
||||
fetch(window.CB_ENDPOINT, {
|
||||
|
|
@ -122,7 +132,7 @@ fn problem_svg(out: &mut String, priority: u32, p: &ProblemView, x: i32) {
|
|||
};
|
||||
let _ = write!(
|
||||
out,
|
||||
"<g id=\"problem-{priority}\"><rect x=\"{x}\" y=\"10\" width=\"120\" height=\"78\" rx=\"8\" \
|
||||
"<g data-drop=\"problem-{priority}\"><rect x=\"{x}\" y=\"10\" width=\"120\" height=\"78\" rx=\"8\" \
|
||||
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>\
|
||||
|
|
@ -182,7 +192,7 @@ fn relations_svg(view: &GroundView) -> String {
|
|||
.map(|f| format!(" \u{2192}{}", seat_name(*f)));
|
||||
let _ = write!(
|
||||
s,
|
||||
"<g id=\"seat-{raw}\"><circle cx=\"{x:.0}\" cy=\"{y:.0}\" r=\"26\" fill=\"#1b1e26\" \
|
||||
"<g data-drop=\"seat-{raw}\"><circle cx=\"{x:.0}\" cy=\"{y:.0}\" r=\"26\" fill=\"#1b1e26\" \
|
||||
stroke=\"{stroke}\" stroke-width=\"2\"/>\
|
||||
<text x=\"{x:.0}\" y=\"{ty:.0}\" fill=\"#dde\" font-size=\"12\" \
|
||||
text-anchor=\"middle\">{name}</text>\
|
||||
|
|
@ -204,7 +214,13 @@ fn player_card(out: &mut String, id: PlayerId, p: &PlayerView, view: &GroundView
|
|||
let is_viewer = view.viewer == Some(id);
|
||||
let _ = write!(
|
||||
out,
|
||||
"<div class=\"card\" data-viewer=\"{is_viewer}\"><b>{name}</b>{you}<br>",
|
||||
// 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,
|
||||
name = seat_name(id),
|
||||
you = if is_viewer { " (you)" } else { "" },
|
||||
);
|
||||
|
|
@ -216,7 +232,7 @@ fn player_card(out: &mut String, id: PlayerId, p: &PlayerView, view: &GroundView
|
|||
);
|
||||
let _ = write!(
|
||||
out,
|
||||
"<span id=\"freedom-{raw}\" class=\"act\"><span class=\"k\">freedom</span> \
|
||||
"<span data-drop=\"freedom-{raw}\" class=\"act\"><span class=\"k\">freedom</span> \
|
||||
{ready}{lifted}</span><br>",
|
||||
raw = id.0,
|
||||
ready = if p.freedom_ready { "READY" } else { "spent" },
|
||||
|
|
@ -422,7 +438,7 @@ pub fn document(
|
|||
if offered {
|
||||
let _ = write!(
|
||||
s,
|
||||
"<div class=\"card act\" id=\"{id}\">drag {a:?} onto a seat, a problem, \
|
||||
"<div class=\"card act\" data-drop=\"{id}\">drag {a:?} onto a seat, a problem, \
|
||||
or the table</div>",
|
||||
id = action_id(a),
|
||||
);
|
||||
|
|
@ -434,18 +450,18 @@ pub fn document(
|
|||
if !spatial {
|
||||
let _ = write!(
|
||||
s,
|
||||
"<div class=\"card btn\" id=\"cmd-{i}\">{}</div>",
|
||||
"<div class=\"card btn\" data-drop=\"cmd-{i}\">{}</div>",
|
||||
esc(&format!("{c:?}"))
|
||||
);
|
||||
}
|
||||
}
|
||||
s.push_str(
|
||||
"</div><div class=\"card\" id=\"table\">the table \u{2014} drop here for an \
|
||||
"</div><div class=\"card\" data-drop=\"table\">the table \u{2014} drop here for an \
|
||||
untargeted action</div>",
|
||||
);
|
||||
}
|
||||
if may_pass {
|
||||
s.push_str("<div class=\"card btn\" id=\"pass\">pass \u{2014} decline to act</div>");
|
||||
s.push_str("<div class=\"card btn\" data-drop=\"pass\">pass \u{2014} decline to act</div>");
|
||||
}
|
||||
|
||||
let _ = write!(
|
||||
|
|
@ -485,6 +501,34 @@ fn json_string(s: &str) -> String {
|
|||
/// 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.
|
||||
/// 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
|
||||
}
|
||||
|
||||
pub fn text_of(html: &str) -> String {
|
||||
let mut out = String::with_capacity(html.len() / 2);
|
||||
let bytes: Vec<char> = html.chars().collect();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue