diff --git a/CHANGELOG.md b/CHANGELOG.md
index fdfe4c8..7b8447e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/Sources/Schema/ParticleRestriction.swift b/Sources/Schema/ParticleRestriction.swift
index e0091ba..bb20d96 100644
--- a/Sources/Schema/ParticleRestriction.swift
+++ b/Sources/Schema/ParticleRestriction.swift
@@ -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 }
diff --git a/Sources/Schema/ParticleRestrictionChoice.swift b/Sources/Schema/ParticleRestrictionChoice.swift
new file mode 100644
index 0000000..a22ede1
--- /dev/null
+++ b/Sources/Schema/ParticleRestrictionChoice.swift
@@ -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(),
+ )
+ }
+}
diff --git a/Tests/Fixtures/schema-differential-xsts-baseline.txt b/Tests/Fixtures/schema-differential-xsts-baseline.txt
index 57bc321..c27e74f 100644
--- a/Tests/Fixtures/schema-differential-xsts-baseline.txt
+++ b/Tests/Fixtures/schema-differential-xsts-baseline.txt
@@ -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
diff --git a/Tests/XSDParticleChoiceRestrictionTests.swift b/Tests/XSDParticleChoiceRestrictionTests.swift
new file mode 100644
index 0000000..d025243
--- /dev/null
+++ b/Tests/XSDParticleChoiceRestrictionTests.swift
@@ -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 {
+ """
+
+ \(base)
+
+ \(derived)
+
+
+
+ """
+ }
+
+ @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: """
+
+
+
+
+ """,
+ derived: "",
+ )
+ #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: """
+
+
+
+
+ """,
+ derived: "",
+ )
+ _ = 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: """
+
+
+
+
+
+
+
+
+
+
+ """,
+ derived: "",
+ )
+ #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: """
+
+
+
+
+
+
+
+
+
+
+ """,
+ derived: "",
+ )
+ _ = 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: """
+
+
+
+
+
+
+
+
+
+
+ """,
+ derived: "",
+ )
+ _ = 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: """
+
+
+
+
+ """,
+ derived: "",
+ )
+ _ = 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: """
+
+
+
+
+ """,
+ derived: "",
+ )
+ #expect(throws: (any Error).self) { try PureXML.Schema.Document(source) }
+ }
+}
diff --git a/Tests/XSTSSuiteExclusions.swift b/Tests/XSTSSuiteExclusions.swift
index dd60fd1..5ccbbb2 100644
--- a/Tests/XSTSSuiteExclusions.swift
+++ b/Tests/XSTSSuiteExclusions.swift
@@ -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 = [
"particlesZ008",
+ "particlesIg004",
]
}
diff --git a/Tests/XSTSSuiteTests.swift b/Tests/XSTSSuiteTests.swift
index 92401b2..89bdfd5 100644
--- a/Tests/XSTSSuiteTests.swift
+++ b/Tests/XSTSSuiteTests.swift
@@ -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
diff --git a/docs/roadmap.md b/docs/roadmap.md
index b889af6..73410c6 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -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) |
diff --git a/docs/xsts-deviations.md b/docs/xsts-deviations.md
index 27caf4e..dbca9ba 100644
--- a/docs/xsts-deviations.md
+++ b/docs/xsts-deviations.md
@@ -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