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 outcome::CheckOutcome;
|
||||||
pub use report::{
|
pub use report::{
|
||||||
A11yEvidence, AdapterStatus, BusUnreachableEvidence, CandidateReport, CostRecord,
|
A11yEvidence, AdapterStatus, BusUnreachableEvidence, CandidateReport, CostRecord,
|
||||||
DependencyDelta, DiffReportRecord, HitTestProbeResult, LocByPart, RegionMassRecord, ReportPart,
|
DependencyDelta, DiffReportRecord, HitTestProbeResult, IntegrationOwnership, LocByPart,
|
||||||
|
RegionMassRecord, ReportPart,
|
||||||
};
|
};
|
||||||
pub use scoring::{
|
pub use scoring::{
|
||||||
criterion_cell, is_eligible, CellOutcome, CHECK_3_RULING, DISQUALIFYING_CHECKS,
|
criterion_cell, is_eligible, CellOutcome, CHECK_3_RULING, DISQUALIFYING_CHECKS,
|
||||||
|
|
|
||||||
|
|
@ -198,13 +198,61 @@ pub struct DependencyDelta {
|
||||||
/// [`crate::scoring::ROUND0_READBACK_EVIDENCE`] and
|
/// [`crate::scoring::ROUND0_READBACK_EVIDENCE`] and
|
||||||
/// [`CandidateReport::check5_bus_unreachable_evidence`] for the field that
|
/// [`CandidateReport::check5_bus_unreachable_evidence`] for the field that
|
||||||
/// actually governs whether check 5 is allowed to be `NotRun`.
|
/// 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)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub enum AdapterStatus {
|
pub enum AdapterStatus {
|
||||||
/// The candidate built and exercised an adapter for this platform.
|
/// 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.
|
/// The candidate did not build an adapter for this platform.
|
||||||
/// **Scope not covered — not a failure.**
|
/// **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 },
|
NotBuilt { platform: String, reason: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -418,6 +466,118 @@ mod tests {
|
||||||
assert!(!matches!(a, AdapterStatus::Implemented { .. }));
|
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
|
/// An unknown field on the wire must be refused, not ignored — the same
|
||||||
/// discipline every deserializable type in this workspace uses.
|
/// discipline every deserializable type in this workspace uses.
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue