Pin-13 schema amendment: adapter rows carry typed integration ownership
Append-only amendment to 694d135. The oracle, the verifier, the scoring
rule, the PASS results and eligibility are all untouched; this changes only
what a cost row is able to say.
The first pair of real candidate reports proved the schema could not carry
what Packet 2B was chartered to record. Both candidates reached AT-SPI
through AccessKit -- C1 via the integration bundled with eframe, C2 via
wiring it wrote by hand -- and they reported that as opposite rows:
C1: NotBuilt accesskit-0.24 "no separate AccessKit-native readback was built"
C2: Implemented accesskit-0.24 "the in-process tree this binary constructs by hand"
C1's row was false as written. The AccessKit path was present and
exercised; every one of its five check-5 readbacks travelled it. NotBuilt
means uncovered scope, and that scope was covered.
Relabelling it Implemented would have fixed the falsehood and still lost
the measurement, because "inherited or candidate-owned?" would have
survived only as prose in notes -- which is precisely how the two
candidates diverged in the first place. So AdapterStatus::Implemented now
requires a typed integration_ownership: Inherited { provider } or
CandidateOwned. Inherited names its provider so the claim can be checked
against the dependency graph instead of taken on trust. NotBuilt keeps no
ownership field at all: a platform reached by any route is Implemented.
Inheriting an integration is not inheriting the semantics drawn on top of
it. A candidate that inherits a bridge still writes the accessible nodes
for whatever it painted itself, and that work is counted under
ReportPart::AccessibilityTreeConstruction, not here -- which is what makes
the two rows comparable rather than a single "has accessibility" flag.
Five tests, each of which fails if its guard is removed: the two-candidate
comparison the old schema could not express, Inherited carrying a
checkable provider, an Implemented row missing integration_ownership
refused at deserialize, NotBuilt refusing an ownership field, and a
round trip that must not alter a row.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RSX4zSLgKvtiXaPjnMqLGz
This commit is contained in:
parent
694d135bfc
commit
d76287cd68
|
|
@ -63,7 +63,8 @@ pub use inputs::{load_all, NeutralInputs, ReferenceFixture};
|
|||
pub use outcome::CheckOutcome;
|
||||
pub use report::{
|
||||
A11yEvidence, AdapterStatus, BusUnreachableEvidence, CandidateReport, CostRecord,
|
||||
DependencyDelta, DiffReportRecord, HitTestProbeResult, LocByPart, RegionMassRecord, ReportPart,
|
||||
DependencyDelta, DiffReportRecord, HitTestProbeResult, IntegrationOwnership, LocByPart,
|
||||
RegionMassRecord, ReportPart,
|
||||
};
|
||||
pub use scoring::{
|
||||
criterion_cell, is_eligible, CellOutcome, CHECK_3_RULING, DISQUALIFYING_CHECKS,
|
||||
|
|
|
|||
|
|
@ -198,13 +198,61 @@ pub struct DependencyDelta {
|
|||
/// [`crate::scoring::ROUND0_READBACK_EVIDENCE`] and
|
||||
/// [`CandidateReport::check5_bus_unreachable_evidence`] for the field that
|
||||
/// actually governs whether check 5 is allowed to be `NotRun`.
|
||||
/// Who *owns* the integration behind an [`AdapterStatus::Implemented`] row.
|
||||
///
|
||||
/// Added by the pin-13 schema amendment (2026-07-30), because the first
|
||||
/// pair of real reports proved that `Implemented`/`NotBuilt` alone cannot
|
||||
/// carry what Packet 2B was chartered to record. C1 reached AT-SPI through
|
||||
/// AccessKit **inherited from eframe** and reported that platform as
|
||||
/// `NotBuilt` ("no separate AccessKit-native readback was built"); C2
|
||||
/// reached AT-SPI through AccessKit **it wired by hand** and reported
|
||||
/// `Implemented`. Same underlying fact, opposite rows — and the one that
|
||||
/// said `NotBuilt` was simply false, since the AccessKit path was present
|
||||
/// and exercised.
|
||||
///
|
||||
/// Relabelling C1's row `Implemented` would have fixed the falsehood and
|
||||
/// still lost the distinction, because "inherited or candidate-owned?"
|
||||
/// would have survived only as prose in `notes` — which is exactly how the
|
||||
/// two candidates diverged in the first place. So it is typed, required on
|
||||
/// every `Implemented` row, and therefore impossible to omit.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub enum IntegrationOwnership {
|
||||
/// The integration came with a dependency the candidate adopted; the
|
||||
/// candidate did not write it. `provider` names what supplied it (e.g.
|
||||
/// `"eframe 0.35 (bundled AccessKit integration)"`), so a reader can
|
||||
/// check the claim against the dependency graph rather than take it.
|
||||
///
|
||||
/// Inheriting an integration is **not** inheriting the semantics drawn
|
||||
/// on top of it: a candidate that inherits a bridge still writes the
|
||||
/// accessible nodes for anything it painted itself, and that work shows
|
||||
/// up in [`ReportPart::AccessibilityTreeConstruction`], not here.
|
||||
Inherited { provider: String },
|
||||
/// The candidate wrote the integration itself — the bridge wiring, the
|
||||
/// event plumbing, the adapter lifecycle.
|
||||
CandidateOwned,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub enum AdapterStatus {
|
||||
/// The candidate built and exercised an adapter for this platform.
|
||||
Implemented { platform: String, notes: String },
|
||||
///
|
||||
/// `integration_ownership` is **required**: a platform the candidate
|
||||
/// actually exposed a tree on is `Implemented` whether the integration
|
||||
/// was inherited or written, and the difference between those two is
|
||||
/// the measurement, not a footnote.
|
||||
Implemented {
|
||||
platform: String,
|
||||
notes: String,
|
||||
integration_ownership: IntegrationOwnership,
|
||||
},
|
||||
/// The candidate did not build an adapter for this platform.
|
||||
/// **Scope not covered — not a failure.**
|
||||
///
|
||||
/// Reserved **exclusively** for uncovered scope. A platform the
|
||||
/// candidate reached — by any route, inherited or its own — is
|
||||
/// `Implemented`, never this.
|
||||
NotBuilt { platform: String, reason: String },
|
||||
}
|
||||
|
||||
|
|
@ -418,6 +466,118 @@ mod tests {
|
|||
assert!(!matches!(a, AdapterStatus::Implemented { .. }));
|
||||
}
|
||||
|
||||
// ---- pin-13 schema amendment: integration ownership ----
|
||||
|
||||
fn implemented(platform: &str, own: IntegrationOwnership) -> AdapterStatus {
|
||||
AdapterStatus::Implemented {
|
||||
platform: platform.to_string(),
|
||||
notes: "n".to_string(),
|
||||
integration_ownership: own,
|
||||
}
|
||||
}
|
||||
|
||||
/// The amendment's whole point: two candidates that both reached a
|
||||
/// platform are both `Implemented`, and the inherited-vs-owned
|
||||
/// difference survives as *data* rather than as prose in `notes`. This
|
||||
/// is the comparison the first pair of real reports could not express —
|
||||
/// C1 reached AT-SPI through AccessKit inherited from eframe and
|
||||
/// reported `NotBuilt`; C2 reached it through AccessKit it wired itself
|
||||
/// and reported `Implemented`.
|
||||
#[test]
|
||||
fn two_candidates_on_one_platform_differ_only_in_ownership() {
|
||||
let c1 = implemented(
|
||||
"accesskit-0.24",
|
||||
IntegrationOwnership::Inherited {
|
||||
provider: "eframe 0.35".to_string(),
|
||||
},
|
||||
);
|
||||
let c2 = implemented("accesskit-0.24", IntegrationOwnership::CandidateOwned);
|
||||
for a in [&c1, &c2] {
|
||||
assert!(matches!(a, AdapterStatus::Implemented { .. }));
|
||||
}
|
||||
let own = |a: &AdapterStatus| match a {
|
||||
AdapterStatus::Implemented {
|
||||
integration_ownership,
|
||||
..
|
||||
} => integration_ownership.clone(),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
assert_ne!(own(&c1), own(&c2));
|
||||
}
|
||||
|
||||
/// `Inherited` must name its provider, so the claim is checkable
|
||||
/// against the dependency graph instead of taken on trust.
|
||||
#[test]
|
||||
fn inherited_carries_its_provider_and_is_not_conflated_with_owned() {
|
||||
let inherited = IntegrationOwnership::Inherited {
|
||||
provider: "eframe 0.35 (bundled AccessKit integration)".to_string(),
|
||||
};
|
||||
match &inherited {
|
||||
IntegrationOwnership::Inherited { provider } => {
|
||||
assert!(provider.contains("eframe"), "{provider}")
|
||||
}
|
||||
IntegrationOwnership::CandidateOwned => panic!("wrong variant"),
|
||||
}
|
||||
assert_ne!(inherited, IntegrationOwnership::CandidateOwned);
|
||||
}
|
||||
|
||||
/// Mutation: an `Implemented` row that omits `integration_ownership`
|
||||
/// must fail to deserialize. If this ever passes, the field has become
|
||||
/// optional in practice and the amendment is decorative — a report
|
||||
/// could once again carry the distinction only in prose.
|
||||
#[test]
|
||||
fn an_implemented_row_without_integration_ownership_is_refused() {
|
||||
let bad = serde_json::json!({
|
||||
"Implemented": { "platform": "at-spi2", "notes": "n" }
|
||||
});
|
||||
let err = serde_json::from_value::<AdapterStatus>(bad)
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert!(err.contains("integration_ownership"), "{err}");
|
||||
}
|
||||
|
||||
/// `NotBuilt` is for uncovered scope only, so it takes no ownership
|
||||
/// field — a platform reached by *any* route is `Implemented`.
|
||||
#[test]
|
||||
fn not_built_takes_no_ownership_field() {
|
||||
let bad = serde_json::json!({
|
||||
"NotBuilt": {
|
||||
"platform": "windows-uia",
|
||||
"reason": "no runner",
|
||||
"integration_ownership": "CandidateOwned"
|
||||
}
|
||||
});
|
||||
let err = serde_json::from_value::<AdapterStatus>(bad)
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert!(err.contains("integration_ownership"), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adapter_rows_round_trip_through_json_both_ways() {
|
||||
for a in [
|
||||
implemented("at-spi2", IntegrationOwnership::CandidateOwned),
|
||||
implemented(
|
||||
"accesskit-0.24",
|
||||
IntegrationOwnership::Inherited {
|
||||
provider: "eframe 0.35".to_string(),
|
||||
},
|
||||
),
|
||||
AdapterStatus::NotBuilt {
|
||||
platform: "windows-uia".to_string(),
|
||||
reason: "no runner".to_string(),
|
||||
},
|
||||
] {
|
||||
let json = serde_json::to_string(&a).unwrap();
|
||||
let back: AdapterStatus = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(
|
||||
serde_json::to_string(&back).unwrap(),
|
||||
json,
|
||||
"round trip changed the row"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// An unknown field on the wire must be refused, not ignored — the same
|
||||
/// discipline every deserializable type in this workspace uses.
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Reference in New Issue