open-standards/aac/aac-rs/crates/aac-conformance/tests/pack.rs
Transaction Science 24093751d4
All checks were successful
standards workspaces build + test / eoc-rs (push) Successful in 1m53s
standards workspaces build + test / jcp-rs (push) Successful in 2m3s
standards workspaces build + test / proof-rs (push) Successful in 1m21s
standards workspaces build + test / sandbox-rs (push) Successful in 2m14s
standards workspaces build + test / smart-byte-rs (push) Successful in 2m31s
standards workspaces build + test / jouleclaw-rs (push) Successful in 11m55s
standards workspaces build + test / aac-rs (push) Successful in 51s
standards workspaces build + test / joule-code-rs (push) Successful in 1m12s
standards workspaces build + test / joulecontract-rs (push) Successful in 1m6s
standards workspaces build + test / map-rs (push) Successful in 2m38s
standards workspaces build + test / mesh-rs (push) Successful in 50s
standards workspaces build + test / openpay (push) Successful in 4m48s
standards workspaces build + test / arl-rs (push) Successful in 5m0s
standards workspaces build + test / joule-ui-rs (push) Successful in 5m0s
standards workspaces build + test / jouledesk-rs (push) Successful in 5m27s
standards workspaces build + test / joulehook-rs (push) Successful in 4m17s
standards workspaces build + test / sense-rs (push) Successful in 3m57s
standards workspaces build + test / macOS energy backend (jcp-energy) (push) Successful in 3m4s
aac: read the published cascade, and check the invariants it declares about itself
gen_vectors writes aac/conformance/v1/cascade.json and nothing read it back, so
the table a third-party implementation certifies against was never checked
against the engine that produces it.

The pack also declares its own invariants -- energy_strictly_monotonic and
offline_levels ["L0","L1","L2"] -- and that block is written as LITERALS by the
generator, not derived from the levels beside it. The pack asserted those
properties; nothing made them true. Regenerate after moving a level across the
offline boundary and the file would ship a new levels array next to the old
claim, contradicting itself and still publishing cleanly.

tests/pack.rs closes both halves. The engine's level table and routing are
rebuilt in process and compared to the published file (rebuilt rather than
regenerated: gen_vectors writes to a hardcoded relative path with no output
argument, so running it to diff would overwrite the pack). Then the declared
invariants are checked against the published data: energy strictly increasing
level by level, and the set of levels actually marked offline equal to the
declared offline_levels.

Two properties worth having in a standard about someone's ability to speak:
CellSelect, SymbolSearch and WordPrediction must route below the offline
boundary -- if they did not, losing connectivity would cost a person their voice,
not a feature -- while the generative operations must stay above it, so the
offline set is not trivially everything. Routing and levels are also checked to
be mutually complete: no route to a level that is not published, no level that
nothing reaches.

Falsified in both directions, restored via git checkout. Marking L3 offline in
the published pack fails the invariant check naming the exact disagreement
(["L0","L1","L2","L3"] vs ["L0","L1","L2"]). Changing is_offline in the engine
fails the drift check. Stated honestly: that second change ALSO fails aac-core's
own unit tests, so unlike sense's C1 this rule was already covered -- the pack
test's addition here is holding the published file to the engine and to its own
declared invariants.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-21 15:16:50 -04:00

210 lines
8.1 KiB
Rust

//! Consume the published `aac` conformance pack.
//!
//! `gen_vectors` writes `aac/conformance/v1/cascade.json` and nothing read it back,
//! so the published cascade — the table a third-party implementation certifies
//! against — was never checked against the engine that produces it.
//!
//! The pack also **declares its own invariants**, and that block is written as
//! literals by the generator rather than derived from the levels beside it:
//!
//! ```text
//! "invariants": { "energy_strictly_monotonic": true, "offline_levels": ["L0","L1","L2"] }
//! ```
//!
//! So the pack asserts those properties; nothing made them true. If `is_offline()`
//! moved a level, the generator would emit the new `levels` array next to the old
//! claim and the file would contradict itself. These tests check the declared
//! invariants against the published data, which is what turns the claim into a fact.
use aac_core::{AacCascade, AacCascadeLevel, AacOperation};
use serde_json::Value;
use std::path::PathBuf;
/// The level order and tags the generator publishes.
const LEVELS: [(AacCascadeLevel, &str); 10] = [
(AacCascadeLevel::CacheHit, "L0"),
(AacCascadeLevel::SymbolLookup, "L1"),
(AacCascadeLevel::LocalPredict, "L2"),
(AacCascadeLevel::ContextPredict, "L3"),
(AacCascadeLevel::SemanticExpand, "L4"),
(AacCascadeLevel::ConversationAi, "L5"),
(AacCascadeLevel::GenerativeComm, "L6"),
(AacCascadeLevel::VoiceClone, "L7"),
(AacCascadeLevel::MultimodalGen, "L8"),
(AacCascadeLevel::AdaptiveModel, "L9"),
];
const OPS: [(AacOperation, &str); 10] = [
(AacOperation::CellSelect, "CellSelect"),
(AacOperation::SymbolSearch, "SymbolSearch"),
(AacOperation::WordPrediction, "WordPrediction"),
(AacOperation::SocialScript, "SocialScript"),
(AacOperation::SentenceConstruction, "SentenceConstruction"),
(AacOperation::ConversationRepair, "ConversationRepair"),
(AacOperation::ComposeLong, "ComposeLong"),
(AacOperation::VoiceBanking, "VoiceBanking"),
(AacOperation::SymbolGeneration, "SymbolGeneration"),
(AacOperation::ModelRetrain, "ModelRetrain"),
];
fn pack() -> Value {
let p = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../../conformance/v1/cascade.json")
.canonicalize()
.expect("the conformance pack must exist next to the spec");
serde_json::from_slice(&std::fs::read(&p).expect("read cascade.json"))
.expect("cascade.json must parse")
}
fn levels(doc: &Value) -> &Vec<Value> {
doc["levels"].as_array().expect("the pack publishes a level table")
}
/// The engine still produces exactly the published cascade.
///
/// Rebuilt in process rather than by running the generator: `gen_vectors` writes to
/// a hardcoded relative path with no output argument, so running it to diff would
/// overwrite the published file.
#[test]
fn the_engine_still_produces_the_published_cascade() {
let doc = pack();
assert_eq!(doc["standard"], "aac");
assert_eq!(doc["vector"], "cascade");
let rebuilt_levels: Vec<Value> = LEVELS
.iter()
.enumerate()
.map(|(i, (lvl, tag))| {
serde_json::json!({
"index": i,
"tag": tag,
"name": lvl.label(),
"energy_uj": lvl.estimated_energy_uj(),
"offline": lvl.is_offline(),
})
})
.collect();
assert_eq!(&rebuilt_levels, levels(&doc), "the level table has drifted from the engine");
let rebuilt_routes: Vec<Value> = OPS
.iter()
.map(|(op, name)| {
let lvl = AacCascade::level_for_operation(*op);
let tag = LEVELS.iter().find(|(l, _)| l == &lvl).map(|(_, t)| *t).expect("known level");
serde_json::json!({ "operation": name, "level": tag })
})
.collect();
assert_eq!(&rebuilt_routes, doc["routing"].as_array().unwrap(), "routing has drifted");
}
/// The invariants the pack declares, checked against the data it ships beside them.
///
/// Both are literals in the generator. Until something compares them to the levels,
/// a pack that contradicts itself would publish cleanly.
#[test]
fn the_declared_invariants_hold_over_the_published_levels() {
let doc = pack();
let inv = &doc["invariants"];
let levels = levels(&doc);
assert_eq!(levels.len(), 10, "a truncated table must not look like a pass");
// Indices and tags are consistent and contiguous.
for (i, l) in levels.iter().enumerate() {
assert_eq!(l["index"].as_u64().unwrap() as usize, i, "level {i} index disagrees");
assert_eq!(l["tag"].as_str().unwrap(), format!("L{i}"), "level {i} tag disagrees");
}
if inv["energy_strictly_monotonic"].as_bool().expect("declared monotonicity") {
for w in levels.windows(2) {
let (a, b) = (w[0]["energy_uj"].as_u64().unwrap(), w[1]["energy_uj"].as_u64().unwrap());
assert!(
b > a,
"the pack declares strictly monotonic energy but {} ({a} uJ) is not below {} ({b} uJ)",
w[0]["tag"],
w[1]["tag"]
);
}
}
let declared: Vec<&str> = inv["offline_levels"]
.as_array()
.expect("declared offline levels")
.iter()
.map(|v| v.as_str().unwrap())
.collect();
let actual: Vec<&str> = levels
.iter()
.filter(|l| l["offline"].as_bool().unwrap())
.map(|l| l["tag"].as_str().unwrap())
.collect();
assert_eq!(
actual, declared,
"the levels marked offline do not match the offline_levels the pack declares"
);
}
/// The accessibility property the cascade exists to protect: the operations a person
/// needs to say something at all must route to a level that works offline.
///
/// Selecting a cell, searching for a symbol and predicting a word are how an AAC user
/// forms basic utterances. If any of those routed above the offline boundary, losing
/// connectivity would cost someone their voice, not merely a feature.
#[test]
fn basic_communication_routes_below_the_offline_boundary() {
let doc = pack();
let offline: Vec<&str> = levels(&doc)
.iter()
.filter(|l| l["offline"].as_bool().unwrap())
.map(|l| l["tag"].as_str().unwrap())
.collect();
let route_of = |op: &str| -> String {
doc["routing"]
.as_array()
.unwrap()
.iter()
.find(|r| r["operation"] == op)
.unwrap_or_else(|| panic!("the pack routes {op}"))["level"]
.as_str()
.unwrap()
.to_string()
};
for op in ["CellSelect", "SymbolSearch", "WordPrediction"] {
let lvl = route_of(op);
assert!(
offline.contains(&lvl.as_str()),
"{op} routes to {lvl}, which the pack marks online — basic communication \
must not depend on connectivity"
);
}
// And the converse, so the offline set is not trivially everything: the expensive
// generative operations are online, which is why they are allowed to cost more.
for op in ["ComposeLong", "VoiceBanking", "ModelRetrain"] {
let lvl = route_of(op);
assert!(!offline.contains(&lvl.as_str()), "{op} unexpectedly routes offline ({lvl})");
}
}
/// Every published operation routes to a level the pack also publishes, and every
/// level is reachable — a routing table naming a level that does not exist, or a
/// level nothing can reach, is a table nobody can implement against.
#[test]
fn routing_and_levels_are_mutually_complete() {
let doc = pack();
let tags: Vec<&str> = levels(&doc).iter().map(|l| l["tag"].as_str().unwrap()).collect();
let routes = doc["routing"].as_array().expect("the pack publishes routing");
assert_eq!(routes.len(), OPS.len(), "routing must cover every operation");
for r in routes {
let lvl = r["level"].as_str().unwrap();
assert!(tags.contains(&lvl), "{} routes to unknown level {lvl}", r["operation"]);
}
for tag in &tags {
assert!(
routes.iter().any(|r| r["level"] == *tag),
"level {tag} is published but no operation reaches it"
);
}
}