From b76cb5824d1ff3fe5517d5fba1e5d12d0347cd30 Mon Sep 17 00:00:00 2001 From: Mihaela Mihaljevic Date: Sun, 28 Jun 2026 00:35:42 +0200 Subject: [PATCH] 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. --- CHANGELOG.md | 4 ++++ Tests/XSTSDifferentialTests.swift | 39 +++++++++++++++++++++++++++---- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67a437f..45f30c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/Tests/XSTSDifferentialTests.swift b/Tests/XSTSDifferentialTests.swift index 47f29f4..0f73644 100644 --- a/Tests/XSTSDifferentialTests.swift +++ b/Tests/XSTSDifferentialTests.swift @@ -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 `/` +/// `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 } }