test(schema): make the libxml2 differential queue unambiguous and characterised (#360)

The schema-validity differential harness (stopper 3) reported each disagreeing
schema by bare basename, but the same name recurs across subdirectories, so an
entry hid which file actually disagreed: Facets/Schemas/gMonth_maxInclusive001
(value="", correctly rejected) and Facets/gMonth/gMonth_maxInclusive001 (the
contested 2001-REC --MM-- form) collapse to one ambiguous line. The report now
uses the path relative to the corpus root.

The header records the triage of the "libxml2 stricter" queue (~66 over msData):
it is dominated by libxml2 over-strictness (a maxOccurs like 999999999999999 is
a valid unbounded-precision nonNegativeInteger libxml2 rejects on its machine-int
limit; an unresolvable include/import) and contested datatype-version readings
(gMonth --MM--, which PureXML accepts to keep valid XSTS instances; anyURI
strictness; a regex {,5} tolerated per iter 7), with a small FP-delicate residue
(base64Binary canonical padding, keyref no-namespace refer). So the queue is
characterised and named, not a backlog of bugs. Characterisation only; the
shipped validator is unchanged, and the false-positive gate stays the XSTS
suite's valid-schemas-rejected == 0.
This commit is contained in:
Mihaela Mihaljevic 2026-06-28 00:35:42 +02:00 committed by GitHub
parent 0af92bcf8d
commit b76cb5824d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 39 additions and 4 deletions

View File

@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Changed
- The schema-validity differential harness (`XSTSDifferentialTests`, the opt-in libxml2 characterisation for production-readiness stopper 3) now reports each disagreeing schema by its path relative to the corpus root, not its bare basename. The same basename recurs across subdirectories (`Facets/Schemas/gMonth_maxInclusive001.xsd`, which carries `value=""` and is correctly rejected, versus `Facets/gMonth/gMonth_maxInclusive001.xsd`, which carries the contested 2001-REC `--MM--` form), so a basename-only entry hid which file actually disagrees. The header now also records the triage of the "libxml2 stricter" queue: it is dominated by libxml2 over-strictness (a `maxOccurs` like `999999999999999` is a valid unbounded-precision integer libxml2 rejects on its machine-int limit; an unresolvable `include`/`import` it fails to load) and contested datatype-version readings (`gMonth --MM--`, accepted to keep valid XSTS instances; `anyURI` strictness), not clean false negatives. Characterisation only; no shipped behaviour changes.
### Fixed
- XSTS invalid-schemas-accepted (#145): a particle restriction whose restricting element has a union-typed base element now applies Type Derivation OK (Simple) (`cos-st-derived-ok`) rather than structural member subsetting. The NameAndTypeOK type clause (§3.9.6) requires the restricting element's type to be validly derived from the base element's type; PureXML accepted a *separately declared* union whenever its member types were a subset of the base union's members, which is not the spec's derivation relation. The restricting type is now valid over a union base only if it is the base type, derives from it through a declared restriction chain, or (clause 2.2.4) validly derives from one of the union's member types. Catches particlesIk011 (a fresh `union(integer)` is not a restriction of `union(integer, string)`): invalid-schemas-accepted 9 to 8, with valid-schemas-rejected held at 0 (verified over the full XSTS; the change is scoped to the element-restriction path, so no attribute or simple-type derivation check moved). Disclosed divergence: particlesIk025/Ik027, where the restricting type derives from a union *member*, are accepted per `cos-st-derived-ok` clause 2.2.4, a contested reading their XSTS `invalid` expectation does not share.

View File

@ -20,6 +20,30 @@ import Testing
/// can also be over-strict) before fixing. The authoritative false-positive gate
/// remains the XSTS suite's W3C-labelled `valid-schemas-rejected` (== 0).
///
/// Triage of that queue (2026-06-28, the ~66 entries over msData): it is itself
/// dominated by libxml2 over-strictness and contested-version readings, NOT clean
/// PureXML false negatives. The categories found, each a deliberate-or-correct
/// PureXML choice, not a bug to fix:
/// - Contested datatype version: `gMonth` value `--MM--` is the original 2001
/// REC lexical form the errata replaced with `--MM`; libxml2 uses the errata,
/// XSTS the 2001 REC, and PureXML accepts `--MM--` precisely to keep valid
/// XSTS instances (the iter-2 "gMonth bonus") -- rejecting it would REGRESS
/// valid-instances-rejected. `anyURI` lexical strictness is the same shape.
/// - libxml2 implementation limit: a `maxOccurs` like `999999999999999` is a
/// valid unbounded-precision `nonNegativeInteger`; libxml2 rejects it on its
/// own machine-int limit. PureXML is CORRECT to accept (groupF/H/J/L/N009).
/// - External-document loading: libxml2 rejects when an `<include>/<import>`
/// `schemaLocation` fails to load; PureXML does not fetch externals here, so
/// it compiles the local document (anyURI_a001 and the test* import chains).
/// - Deliberate under-rejection: a regex `{,5}` quantifier is tolerated as a
/// `.badQuantifier` (iter 7) to avoid over-rejecting valid patterns the engine
/// under-supports (elemE*). The cardinal rule (no false positive) governs.
/// The small residue (a `base64Binary` value whose non-padding bits are non-zero,
/// `LMT=`; a `keyref` `refer` to a no-namespace component from a namespaced
/// schema, idC019) are FP-delicate datatype/identity-constraint candidates, not
/// yet adjudicated. So the actionable yield is small and each candidate must clear
/// this same adjudication; the queue is characterised, not a backlog of bugs.
///
/// xmllint and the corpus are used only in this test harness; neither is a
/// dependency of the shipped package.
@Suite("Schema-validity differential vs libxml2 (opt-in via XSTS_ROOT)")
@ -47,8 +71,8 @@ struct XSTSDifferentialTests {
switch (pureValid, refValid) {
case (true, true): agreeValid += 1
case (false, false): agreeInvalid += 1
case (false, true): pureRejectsRefAccepts.append(name(path))
case (true, false): pureAcceptsRefRejects.append(name(path))
case (false, true): pureRejectsRefAccepts.append(name(path, under: root))
case (true, false): pureAcceptsRefRejects.append(name(path, under: root))
}
}
@ -117,7 +141,14 @@ struct XSTSDifferentialTests {
return result.sorted()
}
private func name(_ path: String) -> String {
(path as NSString).lastPathComponent
/// The path relative to the corpus root for the triage report. The bare
/// basename is ambiguous: the same name recurs across subdirectories (e.g.
/// `Facets/Schemas/gMonth_maxInclusive001.xsd` carries `value=""` and is
/// correctly rejected, while `Facets/gMonth/gMonth_maxInclusive001.xsd`
/// carries the contested 2001-REC `--MM--` form), so a basename-only entry
/// hides which file actually disagrees. Reporting the relative path makes the
/// queue triagable.
private func name(_ path: String, under root: String) -> String {
path.hasPrefix(root) ? String(path.dropFirst(root.count).drop(while: { $0 == "/" })) : path
}
}