CB-WP-0017: legible interaction, and the chaos window's verdict
Some checks failed
ci / check (push) Failing after 4s
Some checks failed
ci / check (push) Failing after 4s
Provenance (tier M, structural S, chaos d4=4 -> OVERRIDE drawn M): the maintainer could drag after CB-WP-0016 but could not tell what was pickable, held, or droppable. Underneath that, the page was WRONG about which moves exist: 9 legal commands rendered as 5 cards each claiming all three target kinds, from a const string in the emitter. Investigate is legal on problems 2 and 3 but not 1; Solve on 1 but not 2 or 3. The live page now says 'Solve onto problem 1'. ADR-0010 restates control 5, which this work would otherwise have outgrown in silence: every game fact the page acts on must arrive from Rust as data; the script may read, match and render it, never compute, infer, filter or default one. The survey's real finding is that the permitted and forbidden designs are indistinguishable from outside, so the vocabulary grep is demoted to a cheap first line and two behavioural properties become the controls -- the highlighted set EQUALS the set Rust emitted, and anything the page marks legal must resolve. Both mutation-proven; the derive-legality mutation produces a plausible highlight (seat-0,1,2 where only seat-1 is legal) and is caught. Visible now: .pick resting shadow, .held on the grabbed element, .dropok on every legal target including BOTH drawings of a seat, and a ghost following the pointer. Nothing perceptual is verified and ADR-0010 D5 says so. The DOM stub now models classList/querySelectorAll/createElement and builds its node set from the real emitted page. Trap recorded: QuickJS fixes its stack limit at Context creation relative to that frame, so a helper returning a Context makes every later eval report 'SyntaxError: stack overflow'. CHAOS WINDOW CLOSED, 12 declarations, 2 overrides, one each way. Both changed the outcome, so the retirement condition is not met. Verdict: keep, and recommend d4 -> d8 with a second window of 12 -- that is a change to the loop's own constraints and is owed to the next declaration as tier-M work, not made here. CB-EV-0014 corrected: it quoted CB-WP-0015 at $15.14/136 and called it the first settled figure quoted. Now $22.70/166. The number had been read during CB-WP-0015 itself, so there are two defects -- the boundary, and quoting from memory instead of re-running the instrument. make all exits 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
ca92db53bc
commit
a55e878bf0
9 changed files with 911 additions and 77 deletions
|
|
@ -56,34 +56,85 @@ fn cards(list: &[games_ground::SolutionCard]) -> String {
|
|||
/// The only JavaScript in the project. See the module docs.
|
||||
pub const SCRIPT: &str = r#"
|
||||
(function () {
|
||||
var down = null;
|
||||
function key(e) {
|
||||
var down = null, held = null, marked = [], ghost = null;
|
||||
|
||||
function node(e) {
|
||||
var n = e.target;
|
||||
while (n && !(n.getAttribute && n.getAttribute('data-drop'))) { n = n.parentNode; }
|
||||
return n ? n.getAttribute('data-drop') : null;
|
||||
return n;
|
||||
}
|
||||
document.addEventListener('pointerdown', function (e) { down = key(e); });
|
||||
function key(n) { return n ? n.getAttribute('data-drop') : null; }
|
||||
function status(t) { document.getElementById('cb-status').textContent = t; }
|
||||
|
||||
// ADR-0010 D1: the destinations come from `data-targets`, which Rust
|
||||
// wrote. This matches on them. It does not compute, infer, filter or
|
||||
// default one -- a script that pattern-matched ids to guess what is
|
||||
// legal would be forbidden even though the visible result is identical.
|
||||
function mark(n) {
|
||||
var spec = n.getAttribute('data-targets');
|
||||
if (!spec) { return; }
|
||||
var want = spec.split(' ');
|
||||
var all = document.querySelectorAll('[data-drop]');
|
||||
for (var i = 0; i < all.length; i++) {
|
||||
if (want.indexOf(all[i].getAttribute('data-drop')) >= 0) {
|
||||
all[i].classList.add('dropok');
|
||||
marked.push(all[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
function clear() {
|
||||
for (var i = 0; i < marked.length; i++) { marked[i].classList.remove('dropok'); }
|
||||
marked = [];
|
||||
if (held) { held.classList.remove('held'); held = null; }
|
||||
if (ghost && ghost.parentNode) { ghost.parentNode.removeChild(ghost); }
|
||||
ghost = null;
|
||||
down = null;
|
||||
}
|
||||
|
||||
document.addEventListener('pointerdown', function (e) {
|
||||
clear();
|
||||
var n = node(e);
|
||||
down = key(n);
|
||||
if (!n || !down) { return; }
|
||||
held = n;
|
||||
n.classList.add('held');
|
||||
mark(n);
|
||||
if (n.getAttribute('data-targets')) {
|
||||
ghost = document.createElement('div');
|
||||
ghost.id = 'cb-ghost';
|
||||
ghost.textContent = n.textContent;
|
||||
ghost.style.left = e.clientX + 'px';
|
||||
ghost.style.top = e.clientY + 'px';
|
||||
document.body.appendChild(ghost);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('pointermove', function (e) {
|
||||
if (!ghost) { return; }
|
||||
ghost.style.left = e.clientX + 'px';
|
||||
ghost.style.top = e.clientY + 'px';
|
||||
});
|
||||
|
||||
document.addEventListener('pointercancel', clear);
|
||||
|
||||
document.addEventListener('pointerup', function (e) {
|
||||
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;
|
||||
var up = key(node(e));
|
||||
var grabbed = down;
|
||||
clear();
|
||||
if (!grabbed || !up) {
|
||||
// Refusing is right; refusing SILENTLY is what let a broken drop
|
||||
// target survive a human sitting in front of it (CB-WP-0016).
|
||||
status(grabbed ? 'took ' + grabbed + ', let go over nothing droppable'
|
||||
: 'nothing droppable under the pointer');
|
||||
return;
|
||||
}
|
||||
var body = 'down=' + encodeURIComponent(down) + '&up=' + encodeURIComponent(up);
|
||||
down = null;
|
||||
var body = 'down=' + encodeURIComponent(grabbed) + '&up=' + encodeURIComponent(up);
|
||||
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;
|
||||
status(t);
|
||||
if (t.indexOf('ok') === 0) { window.location.reload(); }
|
||||
});
|
||||
});
|
||||
|
|
@ -98,11 +149,51 @@ h1,h2{font-size:1rem;margin:1.2rem 0 .4rem;color:#9cf}
|
|||
.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}
|
||||
|
||||
/* CB-WP-0017. Interactive and inert must not look identical: `.pick` is
|
||||
the resting affordance on anything that can be picked up. ADR-0010 D5
|
||||
claims no evidence that this READS as pickable -- that is a human
|
||||
check, and stage 1 is open on it. */
|
||||
.pick{cursor:grab;user-select:none;box-shadow:0 2px 0 #0006,0 0 0 1px #5a7a inset;
|
||||
transition:transform .08s,box-shadow .08s}
|
||||
.pick:hover{box-shadow:0 3px 8px #000a,0 0 0 1px #7ca inset;transform:translateY(-1px)}
|
||||
/* Held: the thing in your hand is lifted and dimmed where it used to be. */
|
||||
.held{cursor:grabbing;opacity:.45;transform:scale(.97)}
|
||||
/* A legal destination for the thing currently held -- and ONLY for that
|
||||
thing. The set is written by Rust into data-targets; the script matches
|
||||
it and never derives it (ADR-0010 D1). */
|
||||
.dropok{outline:2px dashed #9cf;outline-offset:3px;background:#1d2a33}
|
||||
.dropok text{fill:#cfe}
|
||||
/* The ghost that follows the pointer, so a drag is not invisible. */
|
||||
#cb-ghost{position:fixed;pointer-events:none;z-index:9;padding:.3rem .5rem;
|
||||
border-radius:6px;background:#243;border:1px solid #5a7;color:#dde;
|
||||
font:13px ui-monospace,monospace;box-shadow:0 6px 16px #000b;
|
||||
transform:translate(-50%,-140%)}
|
||||
.k{color:#89a}
|
||||
#cb-status{margin-top:1rem;color:#fc9;min-height:1.2em}
|
||||
svg{background:#1b1e26;border:1px solid #445;border-radius:6px}
|
||||
";
|
||||
|
||||
/// Render drop keys as something a player can read.
|
||||
///
|
||||
/// The keys are what the page posts; these are what it shows. Both come
|
||||
/// from the same list, so they cannot disagree about which moves exist.
|
||||
fn target_names(targets: &[String]) -> String {
|
||||
let mut out: Vec<String> = Vec::new();
|
||||
for t in targets {
|
||||
out.push(match t.split_once('-') {
|
||||
Some(("seat", n)) => n
|
||||
.parse::<u8>()
|
||||
.map(|n| seat_name(PlayerId(n)))
|
||||
.unwrap_or_else(|_| t.clone()),
|
||||
Some(("problem", n)) => format!("problem {n}"),
|
||||
_ if t == "table" => "the table".to_string(),
|
||||
_ => t.clone(),
|
||||
});
|
||||
}
|
||||
out.join(", ")
|
||||
}
|
||||
|
||||
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"),
|
||||
|
|
@ -232,7 +323,7 @@ fn player_card(out: &mut String, id: PlayerId, p: &PlayerView, view: &GroundView
|
|||
);
|
||||
let _ = write!(
|
||||
out,
|
||||
"<span data-drop=\"freedom-{raw}\" class=\"act\"><span class=\"k\">freedom</span> \
|
||||
"<span data-drop=\"freedom-{raw}\" class=\"act pick\"><span class=\"k\">freedom</span> \
|
||||
{ready}{lifted}</span><br>",
|
||||
raw = id.0,
|
||||
ready = if p.freedom_ready { "READY" } else { "spent" },
|
||||
|
|
@ -430,17 +521,33 @@ pub fn document(
|
|||
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))
|
||||
// ADR-0010 D1: the legal targets come from `legal` and are
|
||||
// written into the page as data. The script matches on them;
|
||||
// it never derives them. The old text was a CONSTANT —
|
||||
// "onto a seat, a problem, or the table" — emitted whenever
|
||||
// any legal command used this action, and it was wrong
|
||||
// wherever the real target set was narrower, which is almost
|
||||
// everywhere: Investigate is legal on problems 2 and 3 but
|
||||
// not 1 (CB-WP-0017).
|
||||
let targets: Vec<String> = seat
|
||||
.map(|seat| {
|
||||
legal
|
||||
.iter()
|
||||
.filter_map(|c| crate::input::affordance(c, seat))
|
||||
.filter(|(f, _)| *f == action_id(a))
|
||||
.map(|(_, t)| t)
|
||||
.collect()
|
||||
})
|
||||
});
|
||||
if offered {
|
||||
.unwrap_or_default();
|
||||
if !targets.is_empty() {
|
||||
let _ = write!(
|
||||
s,
|
||||
"<div class=\"card act\" data-drop=\"{id}\">drag {a:?} onto a seat, a problem, \
|
||||
or the table</div>",
|
||||
"<div class=\"card act pick\" data-drop=\"{id}\" \
|
||||
data-targets=\"{targets}\">{a:?}<br>\
|
||||
<span class=\"k\">onto</span> {names}</div>",
|
||||
id = action_id(a),
|
||||
targets = esc(&targets.join(" ")),
|
||||
names = esc(&target_names(&targets)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -450,7 +557,7 @@ pub fn document(
|
|||
if !spatial {
|
||||
let _ = write!(
|
||||
s,
|
||||
"<div class=\"card btn\" data-drop=\"cmd-{i}\">{}</div>",
|
||||
"<div class=\"card btn pick\" data-drop=\"cmd-{i}\">{}</div>",
|
||||
esc(&format!("{c:?}"))
|
||||
);
|
||||
}
|
||||
|
|
@ -461,7 +568,9 @@ pub fn document(
|
|||
);
|
||||
}
|
||||
if may_pass {
|
||||
s.push_str("<div class=\"card btn\" data-drop=\"pass\">pass \u{2014} decline to act</div>");
|
||||
s.push_str(
|
||||
"<div class=\"card btn pick\" data-drop=\"pass\">pass \u{2014} decline to act</div>",
|
||||
);
|
||||
}
|
||||
|
||||
let _ = write!(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue