fix(schema): literal RecurseAsIfGroup for a non-repeating base choice

An element restricting a base choice was always checked by the
effective-total-range approximation, which exists for one reason: the
literal section 3.9.6 pairing has no allowance for occurrence counts
accumulated across the choice's own repetitions and over-rejects
W3C-read-valid schemas (particlesZ001, whose manifest calls the rule
ambiguous). That ambiguity is impossible when the choice cannot repeat:
at {max occurs} = 1 every accepted child comes from one traversal of one
branch, so the literal rule governs. elementRestrictsChoice now applies
the literal pairing (the derived element, with its own occurrence range,
must validly restrict some non-pointless branch) when base maxOccurs is
1, and keeps the effective-range reading only for repeating choices.

Closes XSTS particlesHa161 (a derived a{0,1} is not a pointwise
NameAndTypeOK restriction of a required branch a{1,1}) and particlesM034
(a derived d1+ cannot map into the branch (d1*, d2+): the required d2 is
unmapped and not emptiable). The settled-valid neighbors that pin the
boundary from the other side all still pass: Ha160, M002, M003, M035,
and the Z001 shape, each now a regression test in
XSDParticleChoiceRestrictionTests. invalid-schemas-accepted 6 -> 4.

particlesIg004 is reclassified as spec-divergent (4 -> 3): section 3.3.2
maps block="#all" and the explicit substitution/extension/restriction
list to the SAME {disallowed substitutions} set, so NameAndTypeOK clause
5's superset requirement holds and the invalid expectation has no spec
path; it is jointly unsatisfiable with the settled Ig001/002/003 (equal
sets, valid) and Ig005 (#all covers the triple, valid).

The remaining three schema deviations are documented divergences, not
open bugs: Ik025/Ik027 (cos-st-derived-ok 2.2.4 reading) and schN12,
newly adjudicated in docs/xsts-deviations.md: its chameleon redefine
fails src-redefine 6.2.2 on NameAndTypeOK clause 1 (no-namespace locals
vs qualified locals), but 6.2.2 cannot be enforced uniformly because
mgO013's settled-VALID schema also violates it; any uniform enforcement
trades a false negative for a valid-schema-rejected false positive.

Also corrects the stale deviations-doc row that listed the already-closed
particlesIk011 and omitted schN12; the failures file is the authority.

Gates: full XSTS release suite green at 0/3/0/4 (valid buckets unchanged
at zero), libxml2 differential green at 244 pinned entries (Ha161 and
M034 newly pinned, both compile under libxml2), full local gate green.
This commit is contained in:
Mihaela Mihaljevic 2026-07-05 08:58:35 +02:00
parent 2e3d2e25f8
commit 6a7bf78903
9 changed files with 297 additions and 58 deletions

View File

@ -9,6 +9,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- An element restricting a NON-repeating base choice ({max occurs} = 1) is now
checked by the literal RecurseAsIfGroup pairing (XSD 1.0 section 3.9.6): the
derived element, with its own occurrence range, must be a valid restriction
of some branch. The effective-total-range approximation is kept only for a
repeating base choice, where the particlesZ001 ambiguity actually lives
(occurrence counts accumulated across the choice's own repetitions, which the
literal rule cannot express); at {max occurs} = 1 no such accumulation exists
and the spec text governs. Closes XSTS particlesHa161 (an optional derived
element is not a pointwise restriction of a required branch) and
particlesM034 (a derived `d1+` cannot map into a branch `(d1*, d2+)` whose
required `d2` is unmapped and not emptiable; the settled-valid twin
particlesM035, `d2+` over the same branch, still passes), taking
invalid-schemas-accepted 6 -> 4; stricter than libxml2, which implements
neither edge. Separately, particlesIg004 is reclassified as a spec-divergent
schema test (4 -> 3): section 3.3.2 maps `block="#all"` and the explicit
`substitution extension restriction` list to the same {disallowed
substitutions} set, so NameAndTypeOK clause 5's superset requirement holds
and the `invalid` expectation has no spec path (jointly unsatisfiable with
the settled Ig001/002/003 and Ig005 expectations). The remaining three
schema deviations (Ik025, Ik027, schN12) are documented policy divergences
in `docs/xsts-deviations.md`, including why src-redefine 6.2.2 cannot be
enforced uniformly (mgO013's settled-valid schema violates it). Valid
buckets are unchanged at zero.
- A complex-type restriction that swaps an element reference for one of its
substitution-group members is now rejected (NameAndTypeOK, XSD 1.0 section
3.9.6 Elt:Elt clause 1: the declared names must match, and no clause grants

View File

@ -150,7 +150,7 @@ extension PureXML.Schema {
}
}
private static func groupValid(_ restricted: Group, _ base: Group, _ types: [String: ElementType], _ derivation: [String: TypeDerivation]) -> Bool {
static func groupValid(_ restricted: Group, _ base: Group, _ types: [String: ElementType], _ derivation: [String: TypeDerivation]) -> Bool {
// A particle that can never occur (maxOccurs=0) contributes nothing to the
// language, so it is removed before mapping (a "pointless particle"). Left
// in, it would wrongly consume a base particle and starve the particles
@ -316,54 +316,6 @@ extension PureXML.Schema {
lhs.localName == rhs.localName && (lhs.namespaceURI ?? "") == (rhs.namespaceURI ?? "")
}
/// RecurseAsIfGroup against a CHOICE, read by effective total range. The
/// literal §3.9.6 rule under-approximates the ground truth (derived language ⊆
/// base language) and over-rejects a repeating base choice (particlesZ001, whose
/// test doc calls the rule "ambiguous"; W3C reads it valid). First principles:
/// the element validly restricts the choice iff its leaf is a NameAndTypeOK
/// restriction of some element the choice admits AND its effective occurrence
/// range is within the choice's. Accepts `element{0,∞}` over `choice{0,∞}(...)`
/// (Z001) and `c1{2,2}` over a choice whose branch holds `c1` (M002/003); a
/// must-repeat choice (effective min ≥ 2) still rejects a single element
/// (particlesL). A documented, bounded under-rejection (exact in-branch
/// sequencing is not modelled); over-rejection is the non-starter.
private static func elementRestrictsGroup(
_ restricted: Particle,
_ base: Particle,
_ baseGroup: Group,
_ types: [String: ElementType],
_ derivation: [String: TypeDerivation],
) -> Bool {
if baseGroup.compositor == .choice {
return elementRestrictsChoice(restricted, base, baseGroup, types, derivation)
}
// RecurseAsIfGroup for a sequence/all base (§3.9.6): the element as a
// {1,1} synthetic group must be within the base group's range and map to
// a base member inside `groupValid`.
return rangeWithinWildcard(derivedMin: 1, derivedMax: 1, wildcardMin: base.minOccurs, wildcardMax: base.maxOccurs)
&& groupValid(Group(compositor: .sequence, particles: [restricted]), baseGroup, types, derivation)
}
private static func elementRestrictsChoice(
_ restricted: Particle,
_ base: Particle,
_ baseGroup: Group,
_ types: [String: ElementType],
_ derivation: [String: TypeDerivation],
) -> Bool {
let neutral = Particle(minOccurs: 1, maxOccurs: 1, term: restricted.term)
let admitted = elementLeaves(of: baseGroup).contains {
valid(neutral, Particle(minOccurs: 1, maxOccurs: 1, term: $0), types, derivation)
}
return admitted
&& rangeWithinWildcard(
derivedMin: restricted.effectiveOccurrenceMin(),
derivedMax: restricted.effectiveOccurrenceMax(),
wildcardMin: base.effectiveOccurrenceMin(),
wildcardMax: base.effectiveOccurrenceMax(),
)
}
/// Whether every leaf a particle can contain is admitted by `wildcard`: an
/// element by name, a nested wildcard by narrowing.
private static func leavesAdmitted(_ particle: Particle, by wildcard: Wildcard) -> Bool {
@ -376,7 +328,7 @@ extension PureXML.Schema {
/// Whether a derived effective total range fits within a base wildcard's
/// occurrence range: at least the wildcard's minimum, at most its maximum.
private static func rangeWithinWildcard(derivedMin: Int, derivedMax: Int?, wildcardMin: Int, wildcardMax: Int?) -> Bool {
static func rangeWithinWildcard(derivedMin: Int, derivedMax: Int?, wildcardMin: Int, wildcardMax: Int?) -> Bool {
guard derivedMin >= wildcardMin else { return false }
guard let wildcardMax else { return true }
guard let derivedMax else { return false }

View File

@ -0,0 +1,72 @@
extension PureXML.Schema.ParticleRestriction {
/// RecurseAsIfGroup against a CHOICE, split by whether the choice can repeat.
/// A non-repeating choice ({max occurs} = 1) gets the literal §3.9.6 pairing
/// (no occurrence ambiguity exists, so the spec text governs: particlesHa161,
/// particlesM034). A repeating choice is read by effective total range, because
/// the literal rule has no allowance for counts accumulated across the choice's
/// repetitions and over-rejects W3C-read-valid schemas (particlesZ001); the
/// rationale for both regimes is inline in `elementRestrictsChoice`.
static func elementRestrictsGroup(
_ restricted: PureXML.Schema.Particle,
_ base: PureXML.Schema.Particle,
_ baseGroup: PureXML.Schema.Group,
_ types: [String: PureXML.Schema.ElementType],
_ derivation: [String: PureXML.Schema.TypeDerivation],
) -> Bool {
if baseGroup.compositor == .choice {
return elementRestrictsChoice(restricted, base, baseGroup, types, derivation)
}
// RecurseAsIfGroup for a sequence/all base (§3.9.6): the element as a
// {1,1} synthetic group must be within the base group's range and map to
// a base member inside `groupValid`.
return rangeWithinWildcard(derivedMin: 1, derivedMax: 1, wildcardMin: base.minOccurs, wildcardMax: base.maxOccurs)
&& groupValid(PureXML.Schema.Group(compositor: .sequence, particles: [restricted]), baseGroup, types, derivation)
}
private static func elementRestrictsChoice(
_ restricted: PureXML.Schema.Particle,
_ base: PureXML.Schema.Particle,
_ baseGroup: PureXML.Schema.Group,
_ types: [String: PureXML.Schema.ElementType],
_ derivation: [String: PureXML.Schema.TypeDerivation],
) -> Bool {
// A base choice that cannot repeat ({max occurs} = 1) has no occurrence
// ambiguity: every child the type accepts comes from ONE traversal of ONE
// branch, so the literal RecurseAsIfGroup applies as written. The derived
// element (with its own occurrence range) must be a valid restriction of
// some non-pointless branch: for an element branch that is pointwise
// NameAndTypeOK (a derived `a{0,1}` does not restrict a branch `a{1,1}`,
// particlesHa161); for a group branch the element maps into the branch
// with the branch's other members emptiable (a derived `d1+` does not
// restrict `(d1*, d2+)` because every traversal emits a `d2`,
// particlesM034; its twin `d2+` over the same branch is valid,
// particlesM035). The effective-range reading below would accept both.
if base.maxOccurs == 1 {
return rangeWithinWildcard(derivedMin: 1, derivedMax: 1, wildcardMin: base.minOccurs, wildcardMax: base.maxOccurs)
&& baseGroup.particles.filter { $0.maxOccurs != 0 }.contains { valid(restricted, $0, types, derivation) }
}
// A REPEATING base choice is the case the spec's literal rule handles
// badly: §3.9.6 pairs the derived element against one branch with no
// allowance for counts accumulated across the choice's own repetitions,
// which over-rejects e.g. `element{0,∞}` restricting `choice{0,∞}(...)`
// (particlesZ001, whose test doc calls the rule "ambiguous"; W3C reads it
// valid). First principles: the element validly restricts the repeating
// choice iff its leaf is a NameAndTypeOK restriction of some element the
// choice admits AND its effective occurrence range is within the choice's.
// A must-repeat choice (effective min ≥ 2) still rejects a single element
// (particlesL). A documented, bounded under-rejection (exact in-branch
// sequencing is not modelled across repetitions); over-rejection of a
// W3C-read-valid schema is the non-starter.
let neutral = PureXML.Schema.Particle(minOccurs: 1, maxOccurs: 1, term: restricted.term)
let admitted = elementLeaves(of: baseGroup).contains {
valid(neutral, PureXML.Schema.Particle(minOccurs: 1, maxOccurs: 1, term: $0), types, derivation)
}
return admitted
&& rangeWithinWildcard(
derivedMin: restricted.effectiveOccurrenceMin(),
derivedMax: restricted.effectiveOccurrenceMax(),
wildcardMin: base.effectiveOccurrenceMin(),
wildcardMax: base.effectiveOccurrenceMax(),
)
}
}

View File

@ -219,6 +219,8 @@ particlesEa025:../msData/particles/particlesEa025.xsd
particlesEb015:../msData/particles/particlesEb015.xsd
particlesEb040:../msData/particles/particlesEb040.xsd
particlesEc009:../msData/particles/particlesEc009.xsd
particlesHa161:../msData/particles/particlesHa161.xsd
particlesM034:../msData/particles/particlesM034.xsd
positiveInteger_fractionDigits004_1014:../msData/datatypes/Facets/Schemas/positiveInteger_fractionDigits004.xsd
positiveInteger_fractionDigits007_1017:../msData/datatypes/Facets/Schemas/positiveInteger_fractionDigits007.xsd
publicid00201m1:../sunData/Notation/publicId/publicId00201m/publicId00201m1.xsd

View File

@ -0,0 +1,155 @@
import Testing
@testable import PureXML
/// The two regimes of RecurseAsIfGroup against a base choice (XSD 1.0 section
/// 3.9.6): a NON-repeating choice ({max occurs} = 1) gets the literal pairing,
/// a repeating choice keeps the effective-total-range reading (the particlesZ001
/// ambiguity only exists when counts accumulate across repetitions). Shapes are
/// the XSTS cases that pin the boundary from both sides.
@Suite("XSD element-restricts-choice: literal vs effective-range regimes")
struct XSDParticleChoiceRestrictionTests {
private let xsNamespace = "xmlns:xs=\"http://www.w3.org/2001/XMLSchema\""
private func schema(base: String, derived: String) -> String {
"""
<xs:schema \(xsNamespace)>
<xs:complexType name="B">\(base)</xs:complexType>
<xs:complexType name="R">
<xs:complexContent><xs:restriction base="B">\(derived)</xs:restriction></xs:complexContent>
</xs:complexType>
<xs:element name="r" type="R"/>
</xs:schema>
"""
}
@Test("Optional element does not restrict a required branch of a non-repeating choice")
func test_ha161Rejected() {
// particlesHa161: derived a{0,1} vs branch a{1,1} in choice{0,1}. Pointwise
// NameAndTypeOK occurrence fails; the old effective-range reading accepted it.
let source = schema(
base: """
<xs:sequence><xs:choice minOccurs="0">
<xs:element name="a" type="xs:string"/>
<xs:element name="b" type="xs:string"/>
</xs:choice></xs:sequence>
""",
derived: "<xs:sequence><xs:element name=\"a\" type=\"xs:string\" minOccurs=\"0\"/></xs:sequence>",
)
#expect(throws: (any Error).self) { try PureXML.Schema.Document(source) }
}
@Test("Same-occurrence element restricting one branch of a non-repeating choice compiles")
func test_ha160Accepted() throws {
// particlesHa160, the settled-valid twin: a{1,1} vs branch a{1,1}.
let source = schema(
base: """
<xs:sequence><xs:choice>
<xs:element name="a" type="xs:string"/>
<xs:element name="b" type="xs:string"/>
</xs:choice></xs:sequence>
""",
derived: "<xs:sequence><xs:element name=\"a\" type=\"xs:string\"/></xs:sequence>",
)
_ = try PureXML.Schema.Document(source)
}
@Test("Element cannot map into a branch whose required sibling is unmapped")
func test_m034Rejected() {
// particlesM034: derived d1+ vs choice(seq(c1{2,2}, c2?), seq{1,inf}(d1*, d2+)).
// Every traversal of the only d1-bearing branch emits a d2, so pure d1 content
// is not in the base language; the leaf-admission reading accepted it.
let source = schema(
base: """
<xs:choice>
<xs:sequence minOccurs="0" maxOccurs="unbounded">
<xs:element name="c1" minOccurs="2" maxOccurs="2"/>
<xs:element name="c2" minOccurs="0"/>
</xs:sequence>
<xs:sequence maxOccurs="unbounded">
<xs:element name="d1" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="d2" maxOccurs="unbounded"/>
</xs:sequence>
</xs:choice>
""",
derived: "<xs:choice><xs:element name=\"d1\" maxOccurs=\"unbounded\"/></xs:choice>",
)
#expect(throws: (any Error).self) { try PureXML.Schema.Document(source) }
}
@Test("Element maps into a branch whose other siblings are emptiable")
func test_m035Accepted() throws {
// particlesM035, the settled-valid twin of M034: derived d2+ maps onto the
// branch's d2+ with the d1* sibling emptiable.
let source = schema(
base: """
<xs:choice>
<xs:sequence minOccurs="0" maxOccurs="unbounded">
<xs:element name="c1" minOccurs="2" maxOccurs="2"/>
<xs:element name="c2" minOccurs="0"/>
</xs:sequence>
<xs:sequence maxOccurs="unbounded">
<xs:element name="d1" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="d2" maxOccurs="unbounded"/>
</xs:sequence>
</xs:choice>
""",
derived: "<xs:choice><xs:element name=\"d2\" maxOccurs=\"unbounded\"/></xs:choice>",
)
_ = try PureXML.Schema.Document(source)
}
@Test("Element with matching in-branch occurrence restricts a non-repeating choice")
func test_m002ShapeAccepted() throws {
// particlesM002: derived c1{2,2} maps into branch seq{1,3}(c1{2,2}, c2?) of a
// non-repeating choice; the literal regime must keep accepting this.
let source = schema(
base: """
<xs:choice>
<xs:sequence maxOccurs="3">
<xs:element name="c1" minOccurs="2" maxOccurs="2"/>
<xs:element name="c2" minOccurs="0"/>
</xs:sequence>
<xs:sequence>
<xs:element name="d1"/>
<xs:element name="d2" minOccurs="0"/>
</xs:sequence>
</xs:choice>
""",
derived: "<xs:choice><xs:element name=\"c1\" minOccurs=\"2\" maxOccurs=\"2\"/></xs:choice>",
)
_ = try PureXML.Schema.Document(source)
}
@Test("Repeating base choice keeps the effective-range reading")
func test_z001ShapeAccepted() throws {
// particlesZ001's shape: element{0,inf} over choice{0,inf}(...). The literal
// pointwise rule would reject it; W3C reads it valid, and the repeating regime
// must keep accepting counts accumulated across choice repetitions.
let source = schema(
base: """
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="a" type="xs:string"/>
<xs:element name="b" type="xs:string"/>
</xs:choice>
""",
derived: "<xs:choice><xs:element name=\"a\" type=\"xs:string\" minOccurs=\"0\" maxOccurs=\"unbounded\"/></xs:choice>",
)
_ = try PureXML.Schema.Document(source)
}
@Test("Repeating base choice still bounds the effective total range")
func test_ha162ShapeRejected() {
// particlesHa162: a{1,3} over choice{1,2}(a|b): three a's exceed what two
// traversals can produce; the repeating regime's range check rejects it.
let source = schema(
base: """
<xs:sequence><xs:choice maxOccurs="2">
<xs:element name="a" type="xs:string"/>
<xs:element name="b" type="xs:string"/>
</xs:choice></xs:sequence>
""",
derived: "<xs:sequence><xs:element name=\"a\" type=\"xs:string\" maxOccurs=\"3\"/></xs:sequence>",
)
#expect(throws: (any Error).self) { try PureXML.Schema.Document(source) }
}
}

View File

@ -79,7 +79,22 @@ extension XSTSSuiteTests {
/// swap is invalid and Z008's `valid` has no spec text behind it (its annotation
/// asserts "should be valid" without citing one). The schema is still compiled
/// so the group's instance tests run; only its verdict is excluded.
///
/// - `particlesIg004`: NameAndTypeOK clause 5 with base `block="#all"` and derived
/// `block="substitution extension restriction"`, expected `invalid`. Section
/// 3.3.2 maps `#all` to the {disallowed substitutions} value
/// {extension, restriction, substitution}, the identical set the derived
/// element's explicit list maps to, so the two property values are EQUAL and
/// clause 5's superset requirement holds; there is no spec path to `invalid`.
/// The expectation is jointly unsatisfiable with the suite's own settled
/// treatment of the mapped property values: `particlesIg001/002/003` (equal
/// explicit sets, `valid`) and `particlesIg005` (explicit triple restricted to
/// `#all`, `valid`, which requires `#all` to cover the triple). Only a reading
/// where `#all` is a distinct value strictly larger than the complete explicit
/// set reaches `invalid`, and no such value exists in the 1.0 blockSet grammar
/// (`XSDDerivation` compiles `#all` to exactly that triple).
static let specDivergentSchemas: Set<String> = [
"particlesZ008",
"particlesIg004",
]
}

View File

@ -59,7 +59,22 @@ struct XSTSSuiteTests {
/// mismatch (elemZ026, particlesV020). particlesZ008, the identical construct
/// with a settled `valid` expectation contradicting elemZ026's settled `invalid`,
/// is reclassified into `specDivergentSchemas` below.
private let knownSchemaInvalidAccepted = 6
/// 6 -> 3: RecurseAsIfGroup against a NON-repeating base choice ({max occurs} = 1)
/// now applies the literal §3.9.6 pairing instead of the effective-total-range
/// approximation, which only a repeating choice needs (the Z001 ambiguity is about
/// counts accumulated across repetitions, impossible at {max occurs} = 1). Closes
/// particlesHa161 (a derived `a{0,1}` is not a pointwise NameAndTypeOK restriction
/// of a branch `a{1,1}`) and particlesM034 (a derived `d1+` cannot map into the
/// branch `(d1*, d2+)` because the required `d2` is unmapped and not emptiable; its
/// settled-valid twin particlesM035, `d2+` over the same branch, still passes).
/// particlesIg004 (base `block="#all"` vs derived explicit triple) is reclassified
/// into `specDivergentSchemas`: section 3.3.2 maps both to the SAME {disallowed
/// substitutions} set, so clause 5 holds and `invalid` has no spec path. Remaining
/// three: Ik025/Ik027 (the cos-st-derived-ok 2.2.4 divergence above) and schN12
/// (src-redefine 6.2.2 policy, see docs/xsts-deviations.md: enforcing 6.2.2
/// uniformly would reject mgO013's settled-VALID schema, so the FP-safe policy
/// keeps it unenforced and schN12 stays an accepted deviation).
private let knownSchemaInvalidAccepted = 3
private let knownInstanceValidRejected = 0
/// Instance bucket (133 -> 22), per-step deltas in CHANGELOG.md: xsi:type must derive from the declared
/// type; anyType cannot stand in for anySimpleType; an untyped substitutionGroup member inherits its head's

View File

@ -11,10 +11,11 @@ changelog entries and commit messages refer to that lost tracker.
## Current focus (priority order)
1. **[#4](https://codeberg.org/Mihaela/PureXML/issues/4)**: drive invalid
schemas accepted from 6 to 0. The remainder routes through
`ParticleRestriction.violation`'s deliberate under-approximation
(`docs/release-hurdles.md`, Part 1).
1. **[#4](https://codeberg.org/Mihaela/PureXML/issues/4)**: invalid schemas
accepted is at 3, and every remaining case is a documented divergence, not
an open bug: Ik025/Ik027 (cos-st-derived-ok 2.2.4 reading) and schN12
(src-redefine 6.2.2 cannot be enforced uniformly without rejecting
mgO013's settled-valid schema). See `docs/xsts-deviations.md`.
2. **[#5](https://codeberg.org/Mihaela/PureXML/issues/5)**: drive invalid
instances accepted from 4 to 0 (contested-spec and policy-dependent tail,
`docs/xsts-deviations.md`).
@ -35,7 +36,7 @@ together.
| Gate | Now | Issue |
|---|---|---|
| Valid schemas rejected | 0 | at floor, enforced by the ratchet |
| Invalid schemas accepted | 6 | [#4](https://codeberg.org/Mihaela/PureXML/issues/4) |
| Invalid schemas accepted | 3 | [#4](https://codeberg.org/Mihaela/PureXML/issues/4) |
| Valid instances rejected | 0 | at floor, enforced by the ratchet |
| Invalid instances accepted | 4 | [#5](https://codeberg.org/Mihaela/PureXML/issues/5) |

View File

@ -66,8 +66,11 @@ Schema side (10 cases):
|---|---|---|
| `particlesZ030_d` | rejects | Reference-aligned bug, now fixed: a `simpleContent` restriction relaxing a base `required` attribute to `optional` (derivation-ok-restriction.2). invalid-schemas-accepted 10 -> 9. |
| `elemZ026`, `particlesV020` | accept too | Closed 2026-07-05: NameAndTypeOK clause 1 now pairs the DECLARED element references (`Particle.declaredReference` preserves the pre-substitution-expansion identity), so a restriction that swaps a head reference for a substitution-group member is rejected. Stricter than libxml2, spec-grounded (section 3.9.6 Elt:Elt clause 1 has no substitution allowance). The identical construct `particlesZ008` carries a settled `valid` expectation contradicting elemZ026's settled `invalid`; Z008 is reclassified as spec-divergent (excluded), with the full adjudication in `Tests/XSTSSuiteTests.swift`. |
| `particlesHa161`, `particlesIg004`, `particlesIk011/025/027`, `particlesM034` | accept too | Stricter-than-libxml2: NameAndTypeOK / particle-restriction edges (union membership, element-block superset) that libxml2 does not implement. Pursue only if Xerces-level strictness is wanted. |
| `schN12` | (multi-document) | Cross-namespace composition; not triaged in isolation. |
| `particlesIk011` | accept too | Closed at the 9 -> 8 step: NameAndTypeOK over a union base applies cos-st-derived-ok, so a separately-declared union whose members merely subset the base union's members is not a valid derivation. |
| `particlesIk025`, `particlesIk027` | accept too | Disclosed divergence, kept in the count: the restricting type derives from a union MEMBER's base, and PureXML accepts per cos-st-derived-ok clause 2.2.4; the XSTS `invalid` expectation reads the clause more literally (derivation must pass through the anonymous member itself). See the ratchet history in `Tests/XSTSSuiteTests.swift`. |
| `particlesHa161`, `particlesM034` | accept too | Closed 2026-07-05: RecurseAsIfGroup against a NON-repeating base choice ({max occurs} = 1) now applies the literal section 3.9.6 pairing; the effective-total-range approximation is kept only for a repeating choice, where the particlesZ001 ambiguity actually lives (counts accumulated across repetitions). Ha161: a derived `a{0,1}` is not a pointwise NameAndTypeOK restriction of the branch `a{1,1}`. M034: a derived `d1+` cannot map into `(d1*, d2+)` because the required `d2` is unmapped and not emptiable (its settled-valid twin M035, `d2+`, still passes). Stricter than libxml2. |
| `particlesIg004` | accept too | Reclassified spec-divergent (excluded, see `Tests/XSTSSuiteExclusions.swift`): section 3.3.2 maps `block="#all"` and the explicit `substitution extension restriction` to the SAME {disallowed substitutions} set, so NameAndTypeOK clause 5's superset holds and the `invalid` expectation has no spec path; jointly unsatisfiable with the settled Ig001/002/003 (equal sets, valid) and Ig005 (`#all` covers the triple, valid). |
| `schN12` | (multi-document) | Policy-dependent deviation, kept in the count (the schA7 precedent). A chameleon redefine of a no-namespace group whose redefining document is `elementFormDefault="qualified"`: the redefined group's locals land in ns-b while the originals are no-namespace, so src-redefine clause 6.2.2's restriction obligation fails NameAndTypeOK clause 1. PureXML does not enforce 6.2.2, and cannot uniformly: mgO013's settled-VALID schema also violates 6.2.2 (an `all{e1(1,1)}` redefining `all{e1(0,0)}`), so any uniform enforcement rejects a settled-valid schema (a valid-schema-rejected false positive, the hardest floor). The two settled expectations admit no single conformant policy; PureXML keeps the FP-safe one. |
Instance side (sampled): `idZ010`, `mgO013`, `particlesZ001`, `reT17`, `reT38`
are rejected by libxml2 (reference-aligned). `reT17`/`reT38` are now fixed: they