diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2842829..7fcb40d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -31,6 +31,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
+- An unprefixed `xsi:type` value now resolves in the instance's namespace context rather than matching a namespaced type by local name alone. `xsi:type` is a `QName`: a prefix binds through that prefix, an unprefixed name through the in-scope default namespace, and with no default namespace an unprefixed name is in no namespace. PureXML stores each named type under both its namespaced key and a bare local-name alias (for lenient lookup), so an unprefixed `xsi:type="Test"` matched a `{targetNS}Test` through the alias even when the instance had no default namespace, wrongly typing an otherwise-undeclared root and accepting the document. The resolver now keys an unprefixed `xsi:type` to the in-scope default namespace (none here), so `xsi:type="Test"` names no-namespace `Test`, which does not exist; the root element then has neither a declaration nor a usable type and the instance is invalid (W3C XSTS targetNS00101m; libxml2 rejects it identically). A prefixed `xsi:type` and an unprefixed one under a matching default namespace are unaffected. Verified over the full W3C XSTS corpus in release: `invalid-instances-accepted` 6 -> 5, `valid-instances-rejected` held at 0 across all 14383 groups, and the full suite passes.
+
- An XSD `key`/`keyref`/`unique` field that selects a node whose declared type is not simple is now rejected (cvc-identity-constraint.3), rather than wrongly accepted. The XSD identity-constraint rule requires each field to identify a node of simple type; a field that reaches an element declared with no type (the ur-type `anyType`) or an inline complex type without simple content does not, so the instance is invalid (the reference, libxml2, rejects it identically). PureXML accepted such instances because its identity validator could resolve a field target's type only for a global element reached by a direct-child name, not for a local element reached through a path (the failing W3C XSTS case idZ010 nests `anyType` fields several levels deep). A compile-time pass (`XSDParser.identityNonSimpleFieldKeys`) now resolves each field's target element through its selector and field paths, by element-name steps, and records the fields whose declared target is provably non-simple; the validator rejects those at runtime unless the instance gives the node a simple type through `xsi:type` (cvc reads the node's governing type, so a text-only `anyType` element with `xsi:type="xsd:int"` is still a valid field). The resolution is conservative: any wildcard, descendant axis, predicate, `ref`, or named non-builtin type leaves the field unflagged, so a valid field is never rejected. Verified over the full W3C XSTS corpus in release: `invalid-instances-accepted` 8 -> 7 (idZ010 now rejects), `valid-instances-rejected` held at 0 across all 14383 groups (no false positive introduced), and the full suite passes.
- The XSD regex escapes `\i` and `\c` now use the XML 1.0 Second Edition name classes (Appendix B), not the parser's Fifth Edition Name production. XSD 1.0 (Datatypes Appendix F) defines `\i` as the characters that may begin an XML Name (`Letter | '_' | ':'`) and `\c` as the characters that may appear in one (`NameChar`), citing the XML 1.0 Second Edition productions, which are the enumerated Unicode-2.0-era character classes of XML 1.0 Appendix B. `\i`/`\c` were wired to `PureXML.Parsing.XMLCharacter`, which correctly implements the Fifth Edition production used to parse live XML, but the two genuinely differ: the Fifth Edition admits whole blocks where the Second Edition has gaps, so `\c` over-accepted, for example the combining mark `U+0346` (`COMBINING BRIDGE ABOVE`), which Appendix B's `CombiningChar` excludes (its first range ends at `U+0345`). A new `XSDNameCharacter`, transcribed clean-room from the Appendix B grammar (identical across editions one through four) and stored as sorted code-point ranges with binary-search membership, now backs both escapes; the parser's own name check is untouched. The two range tables are verified to reproduce the W3C XSTS `\i`/`\c` enumerations (`reZ005`, `reZ006`, 34516 and 35122 code points) exactly and in both directions, so there is no false-positive surface. Verified over the full W3C XSTS corpus in release: `invalid-instances-accepted` 12 -> 11 (`reZ006i`, whose value is `U+0346`, now rejects), with both false-positive buckets at 0. Found by triaging the invalid-instance tail to a single boundary code point (`U+0345` valid vs `U+0346` invalid) and reading the XML 1.0 Appendix B grammar.
diff --git a/Sources/Schema/ComplexValidatorHelpers.swift b/Sources/Schema/ComplexValidatorHelpers.swift
index 6b73852..46d36cd 100644
--- a/Sources/Schema/ComplexValidatorHelpers.swift
+++ b/Sources/Schema/ComplexValidatorHelpers.swift
@@ -111,17 +111,20 @@ extension PureXML.Schema.ComplexValidator {
return match?.value.trimmingXMLWhitespace()
}
- /// Resolves an instance `xsi:type` attribute to a named type-table key.
+ /// Resolves an instance `xsi:type` attribute to a named type-table key. The
+ /// value is a QName resolved in the instance's namespace context: a prefix
+ /// binds through that prefix, an unprefixed name through the in-scope default
+ /// namespace. With no default namespace an unprefixed name is in no namespace,
+ /// so it keys against the no-namespace type, NOT a namespaced type of the same
+ /// local name (XSTS targetNS00101m: `xsi:type="Test"` with no default namespace
+ /// does not name `{targetNS}Test`).
static func xsiTypeReference(_ element: PureXML.Model.Element, namespaceBindings: [String: String]) -> String? {
guard let value = xsiTypeAttributeValue(element) else { return nil }
let local = value.split(separator: ":").last.map(String.init) ?? value
if value.contains(":"), let prefix = value.split(separator: ":", maxSplits: 1).first.map(String.init), let uri = namespaceBindings[prefix] {
return PureXML.Schema.XSDParser.typeDeclarationKey(local, namespaceURI: uri)
}
- if let uri = namespaceBindings[""] {
- return PureXML.Schema.XSDParser.typeDeclarationKey(local, namespaceURI: uri)
- }
- return local
+ return PureXML.Schema.XSDParser.typeDeclarationKey(local, namespaceURI: namespaceBindings[""])
}
/// The element's character content, trimmed: used to decide whether an
diff --git a/Tests/SchemaXSITypeNamespaceTests.swift b/Tests/SchemaXSITypeNamespaceTests.swift
new file mode 100644
index 0000000..0b42a0f
--- /dev/null
+++ b/Tests/SchemaXSITypeNamespaceTests.swift
@@ -0,0 +1,57 @@
+import Testing
+@testable import PureXML
+
+/// An `xsi:type` value is a QName resolved in the instance's namespace context:
+/// an unprefixed name uses the in-scope default namespace, and with none it is in
+/// no namespace, so it does not name a namespaced type of the same local name.
+/// Mirrors W3C XSTS targetNS00101m (CType): a schema with a `targetNamespace`
+/// declares a type but no element, and a root with `xsi:type` but no element
+/// declaration is valid only when the `xsi:type` QName resolves to that type.
+@Suite("xsi:type QName resolves in the instance namespace context")
+struct SchemaXSITypeNamespaceTests {
+ private func schema() throws -> PureXML.Schema.Document {
+ try PureXML.Schema.Document("""
+
+
+
+
+
+ """)
+ }
+
+ @Test("an unprefixed xsi:type with no default namespace does not name a namespaced type")
+ func test_unprefixedNoDefault() throws {
+ // xsi:type="Test" with no default namespace in scope -> no-namespace Test,
+ // which does not exist; the root `test` has no declaration -> invalid.
+ let xml = """
+ 1
+ """
+ #expect(try !schema().validate(xml).isEmpty)
+ }
+
+ @Test("a prefixed xsi:type resolves through its prefix's namespace")
+ func test_prefixedResolves() throws {
+ let xml = """
+ 1
+ """
+ #expect(try schema().validate(xml).isEmpty)
+ }
+
+ @Test("an unprefixed xsi:type resolves through the in-scope default namespace")
+ func test_unprefixedDefaultNamespace() throws {
+ // A simpleContent type and a default namespace equal to the target namespace:
+ // unprefixed "T" names {targetNS}T and the text content validates, with no
+ // child-element qualification to confound the resolution under test.
+ let schema = try PureXML.Schema.Document("""
+
+
+
+
+
+ """)
+ let xml = """
+ hello
+ """
+ #expect(try schema.validate(xml).isEmpty)
+ }
+}
diff --git a/Tests/XSTSSuiteTests.swift b/Tests/XSTSSuiteTests.swift
index aeb8150..45ee5e7 100644
--- a/Tests/XSTSSuiteTests.swift
+++ b/Tests/XSTSSuiteTests.swift
@@ -91,7 +91,11 @@ struct XSTSSuiteTests {
/// cvc-identity-constraint.3 (idZ010). See ``XSDParser/identityNonSimpleFieldKeys``.
/// 7 -> 6: mgO013.i reclassified as contested (see the excluded set). Its
/// `invalid` is unsatisfiable jointly with its own `valid` schemaTest.
- private let knownInstanceInvalidAccepted = 6
+ /// 6 -> 5: an unprefixed `xsi:type` QName resolves in the instance's namespace
+ /// context, so with no default namespace it names no namespace and does not match
+ /// a namespaced type of the same local name (targetns00101m). See
+ /// ``ComplexValidator/xsiTypeReference(_:namespaceBindings:)``.
+ private let knownInstanceInvalidAccepted = 5
/// Instance tests excluded from the counts because their expected validity is not
/// authoritative, mirroring how the suite's own `status="queried"` entries are
/// skipped. Each must carry a spec-grounded justification; this is never a place to