diff --git a/CHANGELOG.md b/CHANGELOG.md
index de205ee..67a437f 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]
+### 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.
+
## [0.4.8] - 2026-06-27
### Removed
diff --git a/Sources/Schema/ParticleRestrictionElement.swift b/Sources/Schema/ParticleRestrictionElement.swift
index 2be6d89..8a96b39 100644
--- a/Sources/Schema/ParticleRestrictionElement.swift
+++ b/Sources/Schema/ParticleRestrictionElement.swift
@@ -41,8 +41,17 @@ extension PureXML.Schema.ParticleRestriction {
if let baseResolved = resolveType(baseTypeName, types), let derivedResolved = resolveType(restrictedTypeName, types) {
switch (derivedResolved, baseResolved) {
case let (.simple(derivedSimple), .simple(baseSimple)):
- if case .union = baseSimple.variety {
- return PureXML.Schema.XSDParser.isSimpleTypeRestrictionOK(derived: derivedSimple, base: baseSimple)
+ if case let .union(baseMembers) = baseSimple.variety {
+ // cos-st-derived-ok with a union base: the restricting type
+ // validly derives iff it is the base type, derives from it
+ // through a declared restriction chain (typeDerivesByRestriction
+ // returns true for an equal name), or (clause 2.2.4) validly
+ // derives from one of the union's member types. A separately
+ // declared union whose members merely subset the base union's is
+ // NOT a derivation (particlesIk011); structural member subsetting
+ // is not the spec's derivation relation.
+ if typeDerivesByRestriction(restrictedTypeName, baseTypeName, derivation, types) { return true }
+ return baseMembers.contains { PureXML.Schema.XSDParser.isSimpleTypeRestrictionOK(derived: derivedSimple, base: $0) }
}
case (.complex, .simple):
return false
diff --git a/Tests/XSDParticleRestrictionTests.swift b/Tests/XSDParticleRestrictionTests.swift
index 4001ae6..2776e0e 100644
--- a/Tests/XSDParticleRestrictionTests.swift
+++ b/Tests/XSDParticleRestrictionTests.swift
@@ -303,14 +303,18 @@ struct XSDParticleRestrictionTests {
// a union member (xs:string) validly restricts the union-typed base element.
#expect(doc("xs:string", base: "u", types: userTypes) != nil)
- // a union restricting another union (subset members): valid.
let unions = """
"""
- #expect(doc("U1", base: "U2", types: unions) != nil)
- // a union restricting another union but adding/having unmatching members: invalid.
+ // A separately-declared union whose members merely subset the base union's
+ // is NOT a valid derivation: cos-st-derived-ok requires the restricting
+ // type to be the base, derive from it through a declared restriction chain,
+ // or derive from one of its members. Structural member subsetting is not
+ // the spec's derivation relation (cf. XSTS particlesIk011).
+ #expect(doc("U1", base: "U2", types: unions) == nil)
+ // The reverse (U2 has a member U1 lacks) is invalid for the same reason.
#expect(doc("U2", base: "U1", types: unions) == nil)
// a list type restricting a union type: invalid.
#expect(doc("L1", base: "U1", types: unions) == nil)
diff --git a/Tests/XSTSSuiteTests.swift b/Tests/XSTSSuiteTests.swift
index 2327b9a..c99f623 100644
--- a/Tests/XSTSSuiteTests.swift
+++ b/Tests/XSTSSuiteTests.swift
@@ -44,7 +44,13 @@ struct XSTSSuiteTests {
/// a base `required` attribute may not become `optional`) now also runs over a
/// `simpleContent` restriction, not only `complexContent` (particlesZ030_d,
/// which libxml2 also rejects).
- private let knownSchemaInvalidAccepted = 9
+ /// 9 -> 8, NameAndTypeOK over a union base now applies cos-st-derived-ok: a
+ /// separately-declared union whose members merely subset the base union's is
+ /// not a valid derivation (particlesIk011); a genuine restriction of the union
+ /// or of one of its members still passes. (Ik025/Ik027, where the restricting
+ /// type derives from a union member, are accepted per cos-st-derived-ok 2.2.4,
+ /// a disclosed divergence from their contested XSTS `invalid` expectation.)
+ private let knownSchemaInvalidAccepted = 8
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