Selection (access-core; SPEC §5.4, proposed): a prior over the options on the board — usage frequency now, the Laya decision model later — times the person's own switch-timing likelihood, after Nomon (Broderick & MacKay): - press_timing.rs learns a damped kernel-density estimate of each press's offset from the intended highlight, from a population prior scaled to the person's scan interval, and only from selections the person keeps: presses are staged per attempt, dropped when abandoned or undone (including an Undo reached by scanning), and learned only after three newer selections. - selection.rs computes the posterior (late presses explained as late, a 2% lapse term), selects at 99:1 odds with the prior capped so presses must favour the choice 4:1, and otherwise keeps scanning — a row gets up to two extra passes. Without opt-in, or before 10 kept presses, it behaves exactly like the conventional scanner and only measures. - scan_order.rs orders rows, groups and cells by prior mass (fewest expected steps) without moving any; options keep stable position-order ids, and a new order applies from the next pass. dwell.rs weights per-cell dwell from 0.8x to 1.5x within hard bounds. Every adaptation is opt-in and previewable. Conformance C8–C11 (order never moves a position, opt-in required, dwell bounds, selection follows presses): 370/370 with the original 236. Prediction (aac-core): per-language packs — word frequencies, word pairs, sentence starts and a letter model, front-coded and CRC-checked — built reproducibly by tools/lang/build_packs.py from pinned wordfreq 3.2.0 (CC-BY-SA-4.0), FrequencyWords 2018 (CC-BY-SA-4.0) and Common Voice sentences (CC0), into data/lang/ (43 of 45 languages; gu and kn have no open source large enough and report "adaptive only"). PredictionEngine blends a pack with what the person has said (hierarchical Dirichlet smoothing): next word, completion, and letter prediction for letter scanning, deterministically. A per-language pair filter exists; its lists still need curating before the app shows next-word suggestions to children. Co-Authored-By: Claude Opus 5.5 <[email protected]>
93 lines
3.6 KiB
Rust
93 lines
3.6 KiB
Rust
//! The published adaptive vector is what the oracles produce, the reference
|
|
//! engine passes it, and the harness notices when it is tampered with.
|
|
|
|
use std::path::PathBuf;
|
|
|
|
use aac_conformance::{Report, adaptive, default_vector_dir, load_vector, run_with_vectors};
|
|
use serde_json::Value;
|
|
|
|
fn published() -> Value {
|
|
load_vector(&default_vector_dir(), "adaptive.json").expect("conformance/v1/adaptive.json")
|
|
}
|
|
|
|
/// A copy of the published vectors with `adaptive.json` replaced by `doc`.
|
|
fn with_adaptive(doc: &Value, tag: &str) -> PathBuf {
|
|
let dir = std::env::temp_dir().join(format!("aac-conformance-adaptive-{tag}-{}", std::process::id()));
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
for f in ["cascade.json", "scan_rate.json", "dwell.json"] {
|
|
std::fs::copy(default_vector_dir().join(f), dir.join(f)).unwrap();
|
|
}
|
|
std::fs::write(dir.join("adaptive.json"), serde_json::to_vec_pretty(doc).unwrap()).unwrap();
|
|
dir
|
|
}
|
|
|
|
fn failures(r: &Report, prefix: &str) -> Vec<String> {
|
|
r.lines.iter().filter(|l| l.contains("FAIL") && l.contains(prefix)).cloned().collect()
|
|
}
|
|
|
|
#[test]
|
|
fn the_published_vector_is_the_oracles_output() {
|
|
// Byte for byte, as gen-vectors writes it (serde_json's default parser is
|
|
// not correctly rounded, so comparing parsed floats would be unsound).
|
|
let on_disk = std::fs::read_to_string(default_vector_dir().join("adaptive.json")).unwrap();
|
|
let generated = serde_json::to_string_pretty(&adaptive::adaptive_vector()).unwrap() + "\n";
|
|
assert!(on_disk == generated, "run gen-vectors: conformance/v1/adaptive.json is stale");
|
|
}
|
|
|
|
#[test]
|
|
fn the_reference_engine_passes_c8_to_c11() {
|
|
let r = run_with_vectors(&default_vector_dir());
|
|
for c in ["C8", "C9", "C10", "C11"] {
|
|
assert!(failures(&r, c).is_empty(), "{:?}", failures(&r, c));
|
|
assert!(r.lines.iter().any(|l| l.contains(&format!("PASS {c} "))), "{c} ran no checks");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn a_tampered_order_fails_c8() {
|
|
let mut doc = published();
|
|
let units = doc["order_cases"][0]["expected_units"].as_array_mut().unwrap();
|
|
units.swap(0, 1);
|
|
let r = run_with_vectors(&with_adaptive(&doc, "order"));
|
|
assert!(!failures(&r, "C8").is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn a_tampered_decision_fails_c11() {
|
|
let mut doc = published();
|
|
doc["selection_cases"][0]["expected_decision"]["option"] = 2.into();
|
|
let r = run_with_vectors(&with_adaptive(&doc, "decision"));
|
|
assert!(!failures(&r, "C11").is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn a_vector_claiming_adaptation_is_on_by_default_fails_c9() {
|
|
let mut doc = published();
|
|
doc["rule"]["adaptive_order_default"] = true.into();
|
|
let r = run_with_vectors(&with_adaptive(&doc, "default"));
|
|
assert!(!failures(&r, "C9").is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn a_dwell_outside_the_bounds_fails_c10() {
|
|
let mut doc = published();
|
|
doc["dwell_cases"][0]["expected_dwell_ms"][0][2] = 500.into(); // below 640
|
|
let r = run_with_vectors(&with_adaptive(&doc, "dwell"));
|
|
assert!(!failures(&r, "C10").is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn a_vector_publishing_another_offset_window_fails_c11() {
|
|
let mut doc = published();
|
|
doc["rule"]["selection"]["offset_window_intervals"] = serde_json::json!([-0.5, 10.0]);
|
|
let r = run_with_vectors(&with_adaptive(&doc, "window"));
|
|
assert!(!failures(&r, "C11").is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn proposed_checks_are_labelled_and_counted() {
|
|
let r = run_with_vectors(&default_vector_dir());
|
|
let labelled = r.lines.iter().filter(|l| l.contains("[proposed]")).count();
|
|
assert!(labelled > 0 && labelled as u32 == r.proposed, "{labelled} labelled, {} counted", r.proposed);
|
|
assert!(r.lines.iter().filter(|l| l.contains(" C5 ")).all(|l| !l.contains("[proposed]")));
|
|
}
|