diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7110ee0..545eaf0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -28,6 +28,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
+- `xsi:type` naming `anyType` is now rejected on an element whose declared type is
+ `anySimpleType` (cvc-elt.4.3.2.1). `anyType` is the ur-supertype of every type,
+ so it is never validly *derived* from `anySimpleType` (its own subtype). The
+ general not-derived check stays silent on ur-type declared types to avoid false
+ positives; this adds only the provably-invalid `anyType`-for-`anySimpleType`
+ pairing, leaving `anyType` valid on an `anyType`-declared element. XSTS
+ invalid-instances-accepted 30 -> 29 (`stZ056`), no other bucket moved.
+
- `dateTime`, `date`, `gYearMonth`, and `gYear` now reject the year `0000`. XSD
1.0 has no year zero (the year before `0001` is `-0001`), so `0000-01-01T00:00:00`
and the other `0000` forms are not valid values. `DateTimeParser.parseYearOnly`
diff --git a/Sources/Schema/ComplexValidatorDerivation.swift b/Sources/Schema/ComplexValidatorDerivation.swift
index f8e00e6..7bfc90a 100644
--- a/Sources/Schema/ComplexValidatorDerivation.swift
+++ b/Sources/Schema/ComplexValidatorDerivation.swift
@@ -16,6 +16,7 @@ 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 anyTypeSub = anyTypeForAnySimpleTypeError(declared: declared, child: child, at: path, namespaceBindings: namespaceBindings) { return anyTypeSub }
if let listUnion = listUnionSubstitutionError(declared: declared, child: child, at: path, namespaceBindings: namespaceBindings) { return listUnion }
if let abstractType = abstractXsiTypeError(child: child, at: path, namespaceBindings: namespaceBindings) { return abstractType }
if case let .typeReference(name) = declared { return abstractTypeError(named: name, child: child, at: path) }
@@ -66,6 +67,42 @@ extension PureXML.Schema.ComplexValidator {
)
}
+ /// cvc-elt.4.3.2.1 for an `anyType` `xsi:type`: `anyType` is the ur-supertype of
+ /// every type, so it is never DERIVED from `anySimpleType` (its own subtype). An
+ /// `anyType` override on an `anySimpleType`-declared element is therefore invalid.
+ /// The general not-derived check stays silent on ur-type declared types to avoid
+ /// false positives; this rejects only the provably-invalid anyType-for-anySimpleType
+ /// pairing. `anyType` still validly substitutes for an `anyType`-declared element,
+ /// which `declaredIsAnySimpleType` excludes (that resolves to a complex ur-type).
+ func anyTypeForAnySimpleTypeError(
+ 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 .complex(substitute)? = Self.resolveNamedType(reference, in: types),
+ Self.isUrType(substitute),
+ declaredIsAnySimpleType(declared)
+ else { return nil }
+ return PureXML.Validation.ValidationError(
+ reason: "xsi:type '\(label)' is not validly derived from the anySimpleType declared type of '\(child.name.localName)'",
+ at: path,
+ )
+ }
+
+ /// Whether `declared` resolves to the simple ur-type `anySimpleType` itself, not
+ /// `anyType` (a complex ur-type), a list/union, or any more specific type.
+ private func declaredIsAnySimpleType(_ declared: PureXML.Schema.ElementType) -> Bool {
+ let resolved: PureXML.Schema.ElementType? = switch declared {
+ case .simple, .complex: declared
+ case let .typeReference(reference): resolvedDeclaredType(reference)
+ }
+ if case let .simple(type)? = resolved { return type.isAnySimpleType }
+ return false
+ }
+
/// 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
diff --git a/Tests/SchemaListUnionXsiTypeTests.swift b/Tests/SchemaListUnionXsiTypeTests.swift
index 28c99ef..7879786 100644
--- a/Tests/SchemaListUnionXsiTypeTests.swift
+++ b/Tests/SchemaListUnionXsiTypeTests.swift
@@ -113,4 +113,25 @@ struct SchemaListUnionXsiTypeTests {
// Drr restricts Dr: validly derived, accepted.
#expect(try schema.validate(doc("Drr")).isEmpty)
}
+
+ @Test("xsi:type=anyType cannot substitute for an anySimpleType-declared element")
+ func test_anyTypeForAnySimpleType() throws {
+ let xsi = "http://www.w3.org/2001/XMLSchema-instance"
+ let schema = try PureXML.Schema.Document("""
+
+
+
+
+
+
+ """)
+ func root(_ type: String, _ value: String) -> String {
+ #"- \#(value)
"#
+ }
+ // anyType is anySimpleType's supertype, not derived from it: rejected, for that reason.
+ #expect(try schema.validate(root("xs:anyType", "x")).contains { $0.reason.contains("anySimpleType declared type") })
+ // anySimpleType (reflexive) and a derived atomic type are valid substitutes.
+ #expect(try schema.validate(root("xs:anySimpleType", "x")).isEmpty)
+ #expect(try schema.validate(root("xs:int", "123")).isEmpty)
+ }
}
diff --git a/Tests/XSTSSuiteTests.swift b/Tests/XSTSSuiteTests.swift
index de87d9e..2476424 100644
--- a/Tests/XSTSSuiteTests.swift
+++ b/Tests/XSTSSuiteTests.swift
@@ -150,13 +150,13 @@ struct XSTSSuiteTests {
private let knownSchemaValidRejected = 0
private let knownSchemaInvalidAccepted = 43
private let knownInstanceValidRejected = 0
- /// Instance bucket (133 -> 30), per-step deltas in CHANGELOG.md: xsi:type must derive from the
- /// declared type (complex, then atomic and list/union, resolved through the element-ref chain);
- /// block/blockDefault (incl. "substitution" and an untyped head) bar substitutions after the
- /// derivation backbone was re-keyed by namespace; ur-type `xsd:anyType` wildcards are `lax`;
- /// identity fields compare in the decimal value space and take a declared default/fixed when
- /// absent/empty; an attribute wildcard is the INTERSECTION of its sources; year 0000 is invalid.
- private let knownInstanceInvalidAccepted = 30
+ /// Instance bucket (133 -> 29), per-step deltas in CHANGELOG.md: xsi:type must derive from the
+ /// declared type (complex/atomic/list/union via the element-ref chain), and anyType cannot stand
+ /// in for an anySimpleType element (its supertype); block/blockDefault bar substitutions on the
+ /// namespace-keyed backbone; ur-type anyType wildcards are `lax`; identity fields compare in
+ /// decimal value space and take a declared default/fixed when absent/empty; an attribute wildcard
+ /// is the INTERSECTION of its sources; dateTime/date/gYear* reject year 0000.
+ private let knownInstanceInvalidAccepted = 29
@Test("Every XSTS case behaves: compile, reject, validate, invalidate")
func test_suite() throws {
diff --git a/docs/plans/2026-06-18-false-negative-tail.md b/docs/plans/2026-06-18-false-negative-tail.md
index 6b46581..753f629 100644
--- a/docs/plans/2026-06-18-false-negative-tail.md
+++ b/docs/plans/2026-06-18-false-negative-tail.md
@@ -101,7 +101,7 @@ classification is wrong.
| Known particle under-rejection fallout | `particlesZ001/particlesZ001.i` | `ParticleRestriction` |
| Regex instance edges | `reT17/reT17.i`, `reT38/reT38.i`, `RegexTest_422/RegexTest_422.i`, `RegexTest_430/RegexTest_430.i`, `reZ006i/reZ006i.i` | `Sources/Regex/*`, `SimpleType` |
| Schema composition instance fallout | `schA2/schA2.i`, `schA5/schA5.i`, `schA7/schA7.i`, `schU3/schU3.i`, `schU4/schU4.i`, `schU5/schU5.i` | schema composition and instance validator |
-| Simple-type instance edge | `stZ056/stZ056.i` | `SimpleType` |
+| Simple-type instance edge | ~~`stZ056/stZ056.i`~~ closed: `xsi:type=anyType` cannot substitute for an `anySimpleType` element (its supertype) | `ComplexValidatorDerivation` |
| Wildcard instance matching | `wildZ013a/wildZ013a.i`, `wildZ013d/wildZ013d.i` | `ContentMatcher`, wildcard declaration lookup |
## Tasks (ordered)