diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5083afc..a1cc87d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -21,6 +21,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
+- XSTS invalid-instances-accepted (#181, #147): an `xsi:type` naming a list or union simple type is now rejected unless the element's declared type is a ur-type (`anySimpleType`/`anyType`). A list or union type derives only from `anySimpleType` (XSD Part 2), so it cannot validly substitute for an element of any more specific atomic or complex type (`cvc-elt.4.3.2.1`); naming one on, say, an `xs:int`-declared element is invalid. A genuinely derived (restriction) substitute, a list/union under an `anySimpleType` declaration, and a declared type that is itself a list or union are all left alone, so no valid instance is newly rejected. This widening became safe once the derivation backbone was keyed by namespaced identity (#186); the conservative atomic/built-in not-derived cases remain under-rejected pending the full type-identity relation. invalid-instances-accepted 81 → 78, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved.
+
+- XSTS invalid-instances-accepted (#186, #147): the instance-validity derivation and substitution tables are now keyed by namespaced identity (`{ns}local`) instead of bare local name, so two same-local-name types or elements in different namespaces no longer collide. `DerivationTables` keeps its bare-keyed fields for the schema-validity machinery (Particle-Valid-Restriction and the consistency rules) and gains parallel namespaced fields built in the same pass; only the instance consumers (the `xsi:type` blocked/not-derived/abstract checks, the substitution-group closure, and `referenceParticle`) read the namespaced tables, reducing a `type:{ns}local` reference, a bare name, or an element ref to a `{ns}local` key. A key miss stays permissive (no constraint), so the re-keying cannot over-reject; the single-document corpus behaviour is unchanged. With the namespaced backbone in place, a schema's `blockDefault` now supplies the `{disallowed substitutions}` (`block`) of every element and complex type that states none (own `block` overrides; `block=""` clears; intersected with the methods each component admits), without one namespace's defaults shadowing another's. invalid-instances-accepted 89 → 81, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0; an adversarial review exercised same- and cross-namespace substitutions, no-targetNamespace keys, transitive substitution groups, and blockDefault override cases), no other bucket moved.
+
- XSTS invalid-instances-accepted (#147): a complex type's effective attribute wildcard is now the INTERSECTION of the `anyAttribute` wildcards it draws from its own declaration and each referenced attribute group (XSD 1.0 `cos-aw-intersect`), not their union. An attribute is admitted only if every source admits it, so two attribute groups constraining disjoint namespaces (e.g. `urn:a` and `urn:b`) admit nothing and an attribute in either is rejected. A type with a single `anyAttribute` source is unchanged (intersection with no other source is the lone wildcard), and a complexContent extension's union with its base wildcard is unaffected. New `Wildcard.intersection` covers the namespace cases (`##any` identity, enumerated set intersection, enumerated-with-`##other` exclusion) and keeps the less restrictive result for the inexpressible `##other`/`##other` and mismatched `processContents` cases, so the change never over-rejects. invalid-instances-accepted 94 → 89, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved.
- XSTS invalid-instances-accepted (#147): identity-constraint (`unique`/`key`/`keyref`) field values are now compared across the decimal-derived numeric family, so two fields typed by different members of that family with the same number collide. A field typed `xs:decimal` with value `1` and a field typed `xs:unsignedByte` with value `1` (e.g. supplied via `xsi:type`) are the same value and violate a `unique`, where previously a difference in declared base type made them compare unequal. The unification is confined to `xs:decimal` and the integer types, which share one value space; distinct primitive spaces (a number versus a string, or `float` versus `double`) are never unified, so no valid instance is newly rejected. invalid-instances-accepted 95 → 94, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved.
diff --git a/Sources/Schema/ComplexValidatorDerivation.swift b/Sources/Schema/ComplexValidatorDerivation.swift
index ea0c66e..24012dd 100644
--- a/Sources/Schema/ComplexValidatorDerivation.swift
+++ b/Sources/Schema/ComplexValidatorDerivation.swift
@@ -16,10 +16,68 @@ extension PureXML.Schema.ComplexValidator {
) -> PureXML.Validation.ValidationError? {
if let blocked = blockedSubstitutionError(declared: declared, child: child, at: path, namespaceBindings: namespaceBindings) { return blocked }
if let notDerived = notDerivedSubstitutionError(declared: declared, child: child, at: path, namespaceBindings: namespaceBindings) { return notDerived }
+ if let listUnion = listUnionSubstitutionError(declared: declared, child: child, at: path, namespaceBindings: namespaceBindings) { return listUnion }
if case let .typeReference(name) = declared { return abstractTypeError(named: name, child: child, at: path) }
return nil
}
+ /// cvc-elt.4.3.2.1 for a list or union `xsi:type`: a list or union simple type
+ /// is derived only from `anySimpleType`, so it can validly stand in only for an
+ /// element whose declared type is a ur-type (`anySimpleType`/`anyType`). Naming a
+ /// list or union type on an element of any more specific atomic or complex type
+ /// is not a valid derivation. When the declared type is itself a list or union
+ /// the check stays silent (an inline list/union identity cannot be matched), so
+ /// it biases to under-reject and never over-rejects.
+ func listUnionSubstitutionError(
+ declared: PureXML.Schema.ElementType,
+ child: PureXML.Model.Element,
+ at path: [PureXML.Validation.PathKey],
+ namespaceBindings: [String: String],
+ ) -> PureXML.Validation.ValidationError? {
+ guard let label = Self.xsiTypeName(child),
+ let reference = Self.xsiTypeReference(child, namespaceBindings: namespaceBindings),
+ case let .simple(substitute)? = Self.resolveNamedType(reference, in: types),
+ Self.isListOrUnion(substitute),
+ !declaredAdmitsAnySimpleType(declared)
+ else { return nil }
+ return PureXML.Validation.ValidationError(
+ reason: "xsi:type '\(label)' is a list or union type not validly derived from the declared type of '\(child.name.localName)'",
+ at: path,
+ )
+ }
+
+ /// Whether a list or union substitute may validly stand in for `declared`: only
+ /// the ur-types (`anySimpleType`/`anyType`) admit one. A declared type that is
+ /// itself a list or union, or an unresolvable reference, is also treated as
+ /// admitting, so the list/union check stays silent rather than risk a false
+ /// positive on an inline or unknown declared type.
+ private func declaredAdmitsAnySimpleType(_ declared: PureXML.Schema.ElementType) -> Bool {
+ switch declared {
+ case let .simple(type): return type.isAnySimpleType || Self.isListOrUnion(type)
+ case let .complex(type): return Self.isUrType(type)
+ case let .typeReference(reference):
+ guard case let .simple(type)? = Self.resolveNamedType(reference, in: types) else {
+ if case .complex? = Self.resolveNamedType(reference, in: types) { return urReference(reference) }
+ return true
+ }
+ return type.isAnySimpleType || Self.isListOrUnion(type)
+ }
+ }
+
+ /// Whether a reference resolving to a complex type is the complex ur-type.
+ private func urReference(_ reference: String) -> Bool {
+ guard case let .complex(type)? = Self.resolveNamedType(reference, in: types) else { return false }
+ return Self.isUrType(type)
+ }
+
+ /// Whether a simple type is a list or union (deriving only from `anySimpleType`).
+ static func isListOrUnion(_ type: PureXML.Schema.SimpleType) -> Bool {
+ switch type.variety {
+ case .atomic: false
+ case .list, .union: true
+ }
+ }
+
/// The error when an element's declared type is an abstract complex type and
/// the element supplies no `xsi:type` to name a concrete derived type. An
/// abstract type cannot itself be the type of an instance element.
diff --git a/Tests/SchemaListUnionXsiTypeTests.swift b/Tests/SchemaListUnionXsiTypeTests.swift
new file mode 100644
index 0000000..e34cf4b
--- /dev/null
+++ b/Tests/SchemaListUnionXsiTypeTests.swift
@@ -0,0 +1,43 @@
+@testable import PureXML
+import Testing
+
+/// cvc-elt.4.3.2.1 for a list or union `xsi:type`: a list or union simple type
+/// derives only from `anySimpleType`, so it can validly stand in only for an
+/// element whose declared type is a ur-type. Naming a list or union type on an
+/// element of a more specific atomic type is not a valid derivation and is
+/// rejected; a genuinely derived (restriction) substitute, or a list/union under
+/// an `anySimpleType` declaration, is still accepted.
+@Suite("list/union xsi:type derivation")
+struct SchemaListUnionXsiTypeTests {
+ private func schema(declaredType: String) throws -> PureXML.Schema.Document {
+ try PureXML.Schema.Document("""
+
+
+
+
+
+
+ """)
+ }
+
+ private func doc(_ type: String, _ value: String) -> String {
+ #"\#(value)"#
+ }
+
+ @Test("a list or union xsi:type on an int-declared element is rejected")
+ func test_listUnionOnAtomicRejected() throws {
+ #expect(try !schema(declaredType: "xs:int").validate(doc("myList", "1 2")).isEmpty)
+ #expect(try !schema(declaredType: "xs:int").validate(doc("myUnion", "x")).isEmpty)
+ }
+
+ @Test("a valid restriction xsi:type on an int-declared element is accepted")
+ func test_restrictionAccepted() throws {
+ #expect(try schema(declaredType: "xs:int").validate(doc("myInt", "5")).isEmpty)
+ }
+
+ @Test("a list or union xsi:type under anySimpleType is accepted")
+ func test_listUnionUnderUrTypeAccepted() throws {
+ #expect(try schema(declaredType: "xs:anySimpleType").validate(doc("myList", "1 2")).isEmpty)
+ #expect(try schema(declaredType: "xs:anySimpleType").validate(doc("myUnion", "x")).isEmpty)
+ }
+}
diff --git a/Tests/XSTSSuiteTests.swift b/Tests/XSTSSuiteTests.swift
index 83f9715..97dd0d8 100644
--- a/Tests/XSTSSuiteTests.swift
+++ b/Tests/XSTSSuiteTests.swift
@@ -151,12 +151,12 @@ struct XSTSSuiteTests {
private let knownSchemaInvalidAccepted = 69
private let knownInstanceValidRejected = 0
/// Instance bucket: xsi:type must derive from the declared type (133 -> 107);
- /// block="substitution" bars all members even for an untyped head (107 -> 101);
- /// `xsd:anyType` wildcards are `lax` (101 -> 95); identity fields compare in the
- /// decimal value space (95 -> 94); a type's attribute wildcard is the INTERSECTION
- /// of its sources (94 -> 89); `blockDefault` supplies an element's/type's
- /// `block` when it states none, after re-keying derivation by namespace (89 -> 81).
- private let knownInstanceInvalidAccepted = 81
+ /// block="substitution" bars members even for an untyped head (107 -> 101); ur-type
+ /// `xsd:anyType` wildcards are `lax` (101 -> 95); identity fields compare in the decimal
+ /// value space (95 -> 94); attribute wildcard is the INTERSECTION of its sources (94 -> 89);
+ /// `blockDefault` supplies a missing `block` after re-keying derivation by namespace
+ /// (89 -> 81); a list/union xsi:type is not derived from a non-ur type (81 -> 78).
+ private let knownInstanceInvalidAccepted = 78
@Test("Every XSTS case behaves: compile, reject, validate, invalidate")
func test_suite() throws {