PureXML/CHANGELOG.md
Mihaela Mihaljevic 8d30010e0d docs: align stale figures with code and the enforced XSTS gate
- README, roadmap: XSTS status quad and gates table now read the enforced
  0/43/0/31 (Tests/XSTSSuiteTests.swift); test count 986/169 to 1448/258;
  encoding list and the remaining-work section reflect shipped reality;
  check-all expansion lists the validation and changelog gates
- GEMINI.md, onboarding-antigravity.md: valid-schemas-rejected 1 to 0 and the
  pinned baselines updated to current
- SECURITY.md: complete the reporting address
- Parsing.InputEncoding: doc comment lists the full shipped encoding set
- Schema.XSDParserUnion: guard the localName interpolation with ?? "" to match
  the same line and silence an optional-interpolation warning (internal dedup
  key, behavior preserving)
- CHANGELOG: Unreleased entries for the above
2026-06-19 23:32:04 +02:00

220 KiB

Changelog

All notable changes to PureXML are documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

[Unreleased]

Added

  • Added an opt-in differential schema-validity harness against libxml2 (XSTSDifferentialTests, production-readiness stopper #3: characterise correctness against an independent reference, not just the labelled suite). Over 9618 corpus schemas PureXML and libxml2 agree on 4912 (3161 valid, 1751 invalid); the 87 where PureXML is stricter were triaged as PureXML being more correct (libxml2 omits facet consistency, Element Declarations Consistent, whiteSpace restriction, au-props-correct, and notation rules), independently corroborating that PureXML does not over-reject. The 117 where libxml2 is stricter are a false-negative triage queue (stopper #2), written with the rest to /tmp/xsts-differential.txt. xmllint and the corpus are used only in the test harness, never the shipped package.

  • Added the counted-content-automaton design for the interactive resource-bound arc. The design separates the language-recognition, matched-particle attribution, and UPA/determinism claims; replaces occurrence unrolling with counter scopes; states the target O(N * S * D) runtime and O(S * D) live memory bounds; and makes deleting occursUnrollCap, totalStateCap, and positionCap the acceptance criterion.

  • OpenAPIKit-style validation framework completion: BuiltinValidation registry (23 rules), exhaustive fail/succeed isolation tests, configuration pins, public-type and field coverage gates (scripts/check-validation-coverage.sh, scripts/check-validation-fields.sh), warnings channel with strict: promotion, two-tier Validator defaults, XSD schema compile rules as composable validations (SchemaCompileValidation), and ValidationOutcome non-throwing accessor.

Changed

  • Schema compile diagnostics are now located: SchemaError.inconsistent carries [ValidationError] with coding paths that resolve to TreeNode.sourceRange on a ranged schema tree (#169). Parse-time facet and structure findings attach to their declaring nodes where possible; SchemaError.inconsistentFindings exposes the full list. Schematron reuses the shared validationCodingPath() helper.

  • DTD parse-time validity findings are warnings until strict validation promotes them; validateAgainstInternalDTD returns full findings (errors and warnings).

Fixed

  • Silenced an optional-interpolation warning in the union dedup signature (XSDParserUnion): the localName interpolation now guards with ?? "" to match the name-attribute interpolation on the same line. The signature is an internal dedup key compared only against same-run signatures, so the change is behavior-preserving (it removes the Optional("…") rendering, not any key collisions).

  • Documentation accuracy sweep: brought stale current-state figures into line with the code and the enforced gate. The README test count (986/169 -> 1448/258) and XSTS status quad, the roadmap gates table, and the Antigravity onboarding baselines now read the enforced 0 / 43 / 0 / 31; the encoding coverage (the README feature list and the Parsing.InputEncoding doc comment) now lists the full shipped set (the ISO-8859 family, Windows code pages 1250-1258, KOI8-R/U, and the CJK multi-byte encodings) instead of a three-entry subset; the "remaining parity" section now states that libxml2 documented-surface parity is complete (epics #71/#74/#105 closed) with production readiness (#167) as the remaining work; the README check-all.sh expansion lists the validation-coverage, validation-field, and changelog gates; and the SECURITY.md reporting address is corrected.

  • Replaced XSD content-model occurrence unrolling with a counted automaton. ContentMatcher now tracks occurrence counters in active configurations instead of copying particle bodies into states, preserving exact finite {m,n} behavior for high numeric bounds and removing the instance-time silent widening at the old state ceilings. XSD occurrence attributes are preserved as decimal magnitudes beyond Int range so maxOccurs="100000000000000000000" no longer falls back to the default 1.

  • XSTS invalid-schemas-accepted (#145): global element references now carry the referenced declaration's type name, fixed/default constraint, nillable flag, and block metadata into Particle-Valid-Restriction. A local element with an implicit xs:anyType therefore no longer satisfies NameAndTypeOK against a same-named global ref typed as xs:decimal, closing elemZ028e while preserving a valid narrowing to xs:short. invalid-schemas-accepted 44 -> 43, no false positive (valid-schemas-rejected and valid-instances-rejected held at 0), no other bucket moved.

  • XSTS invalid-schemas-accepted (#145): substitution-group closure now stops chaining through an intermediate member whose own block contains substitution. The blocked element itself may still substitute for its direct ancestor, but its descendants are no longer added to higher ancestor heads, so elemZ027_c (a restriction through a -> b(block=substitution) -> c -> d) is rejected while the valid sibling where the blocked member itself is used remains accepted. invalid-schemas-accepted 45 -> 44, no false positive (valid-schemas-rejected and valid-instances-rejected held at 0), no other bucket moved.

  • XSTS invalid-schemas-accepted (#145): a substitution-group head typed xs:anySimpleType now admits simple type members and complex types with simpleContent, but rejects element-only, empty, and mixed complex-type members. The shared derivation oracle treats anySimpleType as a broad ur-type for other restriction checks, so the substitution-group rule now adds its own value-space guard before accepting the affiliation. stZ048 is rejected while the valid simple/simpleContent near-misses remain accepted. invalid-schemas-accepted 46 → 45, no false positive (valid-schemas-rejected and valid-instances-rejected held at 0), no other bucket moved.

  • XSTS invalid-schemas-accepted (#145): a complexContent extension that explicitly changes mixedness, or that adds element content while defaulting to mixed="false", must match the base type's mixed setting. This rejects ctZ010d (mixed base extended with new element content by an omitted/default-false derived type) while preserving the valid ctZ012b near-miss (empty extension inherits the mixed base) and the invalid ctF008 explicit mixed mismatch. invalid-schemas-accepted 47 → 46, no false positive (valid-schemas-rejected and valid-instances-rejected held at 0), no other bucket moved.

  • XSTS invalid-schemas-accepted (#145): a simpleContent restriction that supplies an inline simpleType must use a type that is itself a valid restriction of the base complex type's simple content. A list of xs:int is not a restriction of an atomic xs:decimal content type, so particlesZ018 is now rejected. Valid inline atomic restrictions become the effective simpleContent type for instance validation, including facets declared beside the inline type. invalid-schemas-accepted 48 → 47, no false positive (valid-schemas-rejected and valid-instances-rejected held at 0), no other bucket moved.

  • XSTS invalid-schemas-accepted (#145): a constraining facet may not be applied where a simpleContent restriction's base chain resolves to xs:anySimpleType, the ur simple type, which carries no constraining facets (XSD Part 2 §3.2.1); stZ010 (a complex type extending xs:anySimpleType, restricted by another that adds minLength) is now rejected. The base chain is followed through local complex types' own simpleContent bases; a base that bottoms at any other built-in, a simple type, or an external/unresolved type is left alone, and a restriction supplying its own nested simpleType base is skipped (the facet applies to that), so a facet on a string-, int-, or other real-typed simpleContent stays accepted. invalid-schemas-accepted 49 → 48, no false positive (valid-schemas-rejected and valid-instances-rejected held at 0), no other bucket moved.

  • XSTS invalid-schemas-accepted (#145, #161): a redefinition of an attribute group may not ADD an attribute the original does not declare (a superset is not a restriction, src-redefine / cos-ct-restricts); schM3 (redeclaring the original's attributes plus a new b4) is now rejected. The base is resolved by namespaced identity with a {}name chameleon fallback (a no-targetNamespace redefined schema is absorbed into the redefining namespace). To stay false-positive-free the added-attribute check runs only when the original's attribute set is fully known from its direct declarations (the original has no nested attributeGroup reference) AND the redefinition itself carries no attributeGroup reference: a self-reference brings in the original's attributes, so declaring further ones alongside it is a valid redefine (W3C attgC007/attgC038/groupB018, and test75083/xsd003b), not an addition. invalid-schemas-accepted 51 → 49, no false positive (valid-schemas-rejected and valid-instances-rejected held at 0; five self-reference over-rejections surfaced by the gate were fixed before commit), no other bucket moved.

  • XSTS invalid-schemas-accepted (#145, #161): a non-self-referencing model group redefined inside xs:redefine must be a valid RESTRICTION of the group it redefines (cos-group-restrict / src-redefine 6.1.2), so its content model may not widen the original's language. The check reuses the same particle-restriction oracle as complex-type restriction, resolving the original group by namespaced identity (with a chameleon {}name fallback for a no-targetNamespace redefined schema). Cases now rejected include adding required content to an empty base (schZ006), dropping a required all member so documents without it become valid (schL1), reordering an all (schL5/L6, since the spec's Recurse rule is order-preserving even for all), and growing a group to a superset (schL8), plus the multi-document and chained redefines schN4/5/6/7 and schL10. To stay false-positive-free the check skips a redefinition carrying a self-reference (governed by the self-reference well-formedness rule and not re-parseable here) and declines to judge a base containing a maxOccurs=0 "pointless particle" (the contested normalization case where W3C accepts a redefinition re-enabling that member, mgO013); a genuine restriction (dropping an optional member, an unchanged redefinition) is accepted. invalid-schemas-accepted 64 → 51, no false positive (valid-schemas-rejected and valid-instances-rejected held at 0; an over-rejection of mgO013 surfaced by the gate was fixed before commit), no other bucket moved. Disclosed under-rejections: self-referencing redefinitions (schN10) and maxOccurs=0-base redefines.

  • XSTS invalid-schemas-accepted (#145, #161): a redefinition of an attribute group restricts the original, so it may neither eliminate a required attribute the original declares, nor re-introduce a prohibited one as usable, nor relax a fixed value the original sets (src-redefine / cos-ct-restricts). attgZ002 (dropping the required att1), attgZ003 (re-adding the prohibited att1 as optional), and schM10 (changing a fixed attribute to a default one) are now rejected. Re-declaring a prohibited attribute AS prohibited keeps it prohibited and stays valid, the drop-required check is skipped when the redefinition uses a nested attributeGroup reference (which could re-introduce the attribute), and the fixed-value check fires only when the redefinition declares the matching attribute as non-prohibited with a differing fixed value (keeping the same fixed value, prohibiting it, or omitting it is left alone), so no valid redefinition is rejected. invalid-schemas-accepted 67 → 64, no false positive (valid-schemas-rejected and valid-instances-rejected held at 0), no other bucket moved.

  • A complex type whose simpleContent derives from a list or union simple type now keeps that variety, so its element content is validated as a list or union rather than collapsing to an atomic value space. Previously simpleContentType dropped the base's variety, so an invalid item in a list-typed simpleContent (e.g. a non-int in a list-of-int) was accepted. A valid list/union value is unaffected. (Under-rejection fix surfaced by the libxml2 differential; the W3C suite does not exercise it, so the conformance counts are unchanged.)

  • XSTS invalid-schemas-accepted (#145): a substitution-group member whose type is a list or union derives only from anySimpleType, so it may affiliate only to a head typed anySimpleType (or to the same type, or as a recorded restriction of the head's type). The substitution-type check skipped derivation entirely when either the member or head type was a list/union; a list/union-typed member affiliated to an unrelated head type (addB141: a union member of a list-typed head) is now rejected, while a list/union member of an anySimpleType head and a restriction of a list head remain valid. invalid-schemas-accepted 69 → 67, no false positive (valid-schemas-rejected and valid-instances-rejected held at 0), no other bucket moved.

  • XSTS invalid-instances-accepted (#147): a block on a LOCAL element declaration is now keyed by the element's instance (qualified) name in the derivation backbone, so an xsi:type substitution blocked on an unqualified local element is barred. The namespaced backbone keyed every element by the target namespace, but a local element is in the target namespace only when qualified (its own form, else the schema's elementFormDefault); an unqualified local element's block went unfound (typeDef00801). invalid-instances-accepted 32 → 31, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved.

  • XSTS invalid-instances-accepted (#147): an instance xsi:type may not name an abstract type. An abstract type cannot be the type of an instance element (cvc-elt.4.3.2), so it is invalid as a substitution even when the declared type would otherwise admit it (typeDef01101); a concrete type is unaffected. This complements the existing check for an abstract DECLARED type with no xsi:type. invalid-instances-accepted 34 → 32, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved.

  • XSTS invalid-instances-accepted (#147): an element with a fixed value constraint and a mixed content type must carry its fixed character value with no element children (cvc-elt.5.2.2.2.1). The fixed check validated only the concatenated text, so a mixed fixed element whose text matched but which also contained child elements (ctZ013) was accepted; element children are now rejected. An empty element (which takes the fixed value) and text equal to the fixed value remain valid. invalid-instances-accepted 37 → 34, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved.

  • XSTS invalid-instances-accepted (#147): a simpleContent derivation whose base is another complex type with simpleContent now resolves through the chain to the underlying simple type, so the element's text is validated against it. A type restricting or extending a complex type that ultimately derives from a built-in (e.g. Test restricts Test2 which extends xs:int) validates its content as that built-in rather than as unconstrained string; baseTD00101's non-int text is now rejected. The base resolution recurses through complexTypeNodes, accumulating facets, with a cycle guard. invalid-instances-accepted 39 → 37, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved.

  • XSTS invalid-instances-accepted (#147): use="prohibited" on an attribute now excludes it from the complex type's effective {attribute uses} (XSD 1.0 §3.4.2) during instance validation, so a prohibited attribute is undeclared: its presence is rejected when no attribute wildcard admits it (attJ), and still admitted (and validated per the wildcard's processContents) when one does (attZ002). The prohibited declaration is kept in the type rather than dropped, so schema-validity checks still see it (the invalid-schemas-accepted count is unchanged). invalid-instances-accepted 43 → 39, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved.

  • XSTS invalid-instances-accepted (#147): an element may now carry at most one attribute whose type is xs:ID (XSD 1.0 §3.4.6). Two ID-typed attributes admitted through an xs:anyAttribute wildcard (the schema itself stays valid, as a type with two declared ID attribute uses is already a compile error) are rejected at instance time. The count covers both a matched declared use's type and the global declaration a matching wildcard admits, including a restriction of xs:ID; xs:IDREF/xs:ENTITY and a lone ID attribute are unaffected. invalid-instances-accepted 45 → 43, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved.

  • XSTS invalid-instances-accepted (#146, #147): an identity-constraint (unique/key/keyref) field that selects an absent attribute or an empty element whose declaration carries a default/fixed now takes that value as its identity-tuple component, so two members can collide on the defaulted value (idG011/idG012 default, idF016/idF017 fixed). The schema's default/fixed value constraints on identity-field targets are gathered at compile time, keyed the same way as the field types, and threaded through to the identity validator. The substitution is exact about absent versus present-empty: an @attr field is defaulted only when the attribute is ABSENT (empty node-set), never when it is present with value ""; a . element field is defaulted only when the selected element has no character content. Applying a default only adds an identity value where the schema declares one, so it can create a genuine duplicate-key detection but never reject a valid instance. invalid-instances-accepted 50 → 45, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved.

  • XSTS invalid-instances-accepted (#147): a block on a substitution-group head's TYPE, not only on the head element, now bars a member from substituting. The substitution-group filter drops a member whose type reaches the head's type by a blocked derivation method, using the union of the head element's {disallowed substitutions} and the head type's {prohibited substitutions}; previously only the head element's own block was consulted, so a block="restriction" on the head's complex type let a restriction-derived member substitute. The element-level block="substitution" outright bar is unchanged, and a member derived by a method the block does not list still substitutes. invalid-instances-accepted 53 → 50, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved.

  • XSTS invalid-instances-accepted (#181, #147): the xsi:type not-derived check now also flags a complex substitute that records no base of its own. Such a type derives only from anyType, so it cannot validly substitute for a non-ur complex declared type; naming the declared type's own complex ancestor, or an unrelated baseless complex type, is rejected. A complex substitute is in scope even when absent from the derivation backbone (its non-derivation from a non-ur type is certain), while an atomic substitute must still be in the backbone so a built-in stays silent. A genuine extension/restriction substitute is accepted, and a baseless complex substitute under an anyType declaration is admitted. invalid-instances-accepted 60 → 53, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved.

  • XSTS invalid-instances-accepted (#181, #147): the xsi:type not-derived check (cvc-elt.4.3.2.1) now covers ATOMIC simple substitutes, not only complex ones. An xsi:type naming an atomic type that is not derived from the element's declared type, such as a sibling restriction or the declared type's own ancestor, is rejected. The check still fires only when both the substitute and the declared type are recorded in the namespaced derivation backbone with no path between them, so a built-in substitute or declared type (absent from the backbone) and the ur-types stay silent (under-reject), avoiding any built-in-derivation false positive; a genuinely derived restriction substitute is accepted. invalid-instances-accepted 66 → 60, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved.

  • 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. The declared type is resolved through the element-ref / type-reference chain, so the check also fires for an element reached via <xs:element ref> whose global declaration is atomic. 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 → 66, 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.

  • XSTS invalid-instances-accepted (#146, #147): the ur-type xsd:anyType now processes its element and attribute wildcards as lax, not skip (XSD 1.0 §3.4.7). A child element or attribute of an untyped element (or an element typed xs:anyType) that has a global declaration is validated against it; undeclared content is still admitted. Previously such content was skipped entirely, so an invalid declared child (e.g. a duration-typed attribute with value "P" reached through <xsd:anyAttribute/>) was accepted. The same change required making the wildcard lookup of a global element declaration namespace-exact: a global is always in the schema target namespace, so an unqualified instance element matches only a no-namespace global (the qualified key element:{}local already covers it); the previous bare-local-name fallback conflated namespaces and would have wrongly validated an unqualified child against a same-named namespaced declaration. invalid-instances-accepted 101 → 95, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0; the spec's own errC007 anyType-lax case validates correctly), no other bucket moved.

  • XSTS invalid-instances-accepted (#147): block="substitution" (or a block set containing it, including #all) on a substitution-group head now bars every member from substituting even when the head is an abstract, untyped element. The substitution block is type-independent, but filterSubstitutions previously applied it only to a head that also had a type, so an untyped head's members still substituted. invalid-instances-accepted 107 → 101, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved.

  • XSTS invalid-instances-accepted (#181, #147): an instance xsi:type naming a complex type must be derived from the element's declared type (cvc-elt.4.3.2.1). A complex xsi:type on a different branch of the type hierarchy (not derived from the declared type) is now rejected, where previously only a derived-but-blocked substitution was caught. Conservatively scoped to substitutes that resolve to a complex type recorded in the derivation backbone, so simple/list/union substitutes and unresolvable references are left alone (no over-rejection). invalid-instances-accepted 133 → 107, no false positive (valid-instances-rejected held at 0, valid-schemas-rejected held at 0), no other bucket moved. Disclosed bound: the derivation backbone is keyed by bare local name (shared with the existing block-substitution rule), so a cross-namespace same-local-name collision could in principle be misjudged; the corpus exercises none, and the holistic fix is to key the backbone by namespaced type identity.

  • XSTS invalid-schemas-accepted (#145): an xs:include and xs:redefine require a schemaLocation attribute (only xs:import's is optional); one declared without it is rejected. invalid-schemas-accepted 71 → 69, no false positive (valid-schemas-rejected held at 0; a required-attribute check with no name resolution cannot over-reject), no other bucket moved.

  • XSTS invalid-schemas-accepted (#161): a schema referenced by xs:redefine must have the same targetNamespace as the redefining schema, or none (chameleon), exactly as for xs:include; redefining a different-namespace schema is rejected. The existing include-namespace-matching check now also covers redefine, selecting the loaded redefined schema document (not the redefine wrapper) so its real target namespace is compared. invalid-schemas-accepted 74 → 71, no false positive (valid-schemas-rejected held at 0; an adversarial review confirmed same-namespace, chameleon, and no-namespace redefines are not flagged), no other bucket moved.

  • XSTS invalid-schemas-accepted (#145): an identity-constraint selector/field xpath may not use an undeclared namespace prefix. Every prefix in a name test must be a declared xmlns: prefix in scope (the implicit xml prefix excepted); a selector/field whose xpath names an unbound prefix is rejected. invalid-schemas-accepted 77 → 75, no false positive (valid-schemas-rejected held at 0; an adversarial review confirmed declared prefixes on the schema root or any ancestor resolve correctly), no other bucket moved.

  • XSTS invalid-schemas-accepted (#161): a group or attributeGroup inside xs:redefine may reference itself at most once, and a group self-reference must occur exactly once, minOccurs = maxOccurs = 1 (src-redefine.6.1.1/6.1.2/7.2.1). Two self-references, or a group self-reference with minOccurs="0", maxOccurs="unbounded", or any count above 1, is rejected. The self-reference is matched by namespace (not local name alone) and the search is bounded to the component's own model (stopping at nested element/complexType scopes), so a reference to an imported same-named component, or a recursive reference inside a nested element's content, is not flagged (no false positive). invalid-schemas-accepted 79 → 74, no false positive (valid-schemas-rejected held at 0), no other bucket moved.

  • XSTS invalid-schemas-accepted (#145): a local element declaration must identify itself with a name or a ref; an element with neither is rejected. invalid-schemas-accepted 80 → 79, no false positive (valid-schemas-rejected held at 0), no other bucket moved.

  • XSTS invalid-schemas-accepted (#145): a complexType nested in an element is a local, anonymous type and may not carry a name attribute; only a top-level complexType (or a redefine redefinition) is named. invalid-schemas-accepted 81 → 80, no false positive (valid-schemas-rejected held at 0), no other bucket moved.

  • XSTS invalid-schemas-accepted (#161): a type inside xs:redefine must restrict or extend the type it redefines, which lives in the redefining schema's own target namespace (src-redefine.5). A derivation base bound to a different namespace, such as an imported type that happens to share the redefined type's local name, is not a self-redefinition and is now rejected; the previous check compared local names only and accepted it. A base with no resolvable namespace is left alone, so an unprefixed or own-namespace self-reference is unaffected. invalid-schemas-accepted 84 → 81, no false positive (valid-schemas-rejected held at 0; differential PureXML-stricter unchanged at 87 over 9618 schemas), no other bucket moved.

  • XSTS invalid-schemas-accepted (#161): an import's namespace attribute must equal the targetNamespace of the schema document it resolves to (src-import.3.1/3.2): both present and equal, or both absent. Importing a no-namespace document under a declared namespace, or a document whose target namespace differs from the declared one, is now rejected. Checked only when the schemaLocation resolves to a loaded schema (an unresolved import is the processor's choice and not an error); imports keep their own target namespace, so no chameleon adjustment applies. invalid-schemas-accepted 87 → 84, no false positive (valid-schemas-rejected held at 0; differential PureXML-stricter unchanged at 87 over 9618 schemas), no other bucket moved.

  • XSTS invalid-schemas-accepted (#145, #161): a resolved include/import/redefine schemaLocation must be a well-formed schema document (src-resolve). When the schema loader returns content for a referenced location, that content is now required to parse as an xs:schema; not-well-formed XML, or well-formed XML whose root is not xs:schema, makes the composing schema invalid. A location the loader does not resolve (returns nil) is not an error, since resolution is the processor's choice, so a self-contained schema is unaffected. invalid-schemas-accepted 95 → 87, no false positive (valid-schemas-rejected held at 0; differential PureXML-stricter unchanged at 87 over 9618 schemas), no other bucket moved.

  • XSTS invalid-schemas-accepted (#145): an attribute's type must be a simple type (au-props-correct / src-attribute). An attribute whose type resolves to a complex type, or to the complex ur-type xs:anyType, is rejected; a built-in or user simple type, an inline simpleType, or no type (anySimpleType) stays valid. A locally-declared type takes precedence over the built-in reading. invalid-schemas-accepted 96 → 95, no false positive (valid-schemas-rejected held at 0; differential PureXML-stricter unchanged at 87 over 9618 schemas), no other bucket moved.

  • XSTS invalid-schemas-accepted (#145): an element's default/fixed value constraint requires a content type that admits character data (cos-valid-default). An element whose type is, or whose inline complex type has, element-only or empty content may not carry a value constraint; a simple type, mixed content, the ur-type xs:anyType, and an untyped element (also anyType) stay valid. The check flags only a content type that is certainly element-only or empty, so it cannot over-reject. invalid-schemas-accepted 99 → 96, no false positive (valid-schemas-rejected held at 0; differential PureXML-stricter unchanged at 87 over 9618 schemas), no other bucket moved.

  • XSTS invalid-schemas-accepted (#145): the base-type kind of a content-model derivation is enforced. A complexContent restriction or extension base must be a complex type (src-ct.1), so a simpleType or a built-in simple type such as xs:string is rejected (the ur-type xs:anyType is complex and stays valid). A simpleContent extension base must be a simple type or a complex type with simple content (src-ct.2), so a complex type with element-only, mixed, or empty content, including xs:anyType, is rejected. A locally-declared type takes precedence over the built-in reading, so a schema that targets the XSD namespace and extends its own complex types (the schema-for-schemas) is not rejected. invalid-schemas-accepted 104 → 99, no false positive (valid-schemas-rejected held at 0, corroborated by the differential: PureXML-stricter unchanged at 87 over 9618 schemas), no other bucket moved.

  • XSTS invalid-schemas-accepted (#145): an identity constraint's content model is enforced. unique, key, and keyref must contain exactly one selector followed by one or more fields (after an optional annotation); a field before the selector, a second selector, or no field is now rejected (previously only child-element membership was checked, not their cardinality or order). invalid-schemas-accepted 110 → 104, no false positive (valid-schemas-rejected held at 0), no other bucket moved.

  • XSTS invalid-schemas-accepted (#145): the occurrence-range constraints on model groups are enforced. A particle's minOccurs may not exceed its maxOccurs, both defaulting to 1 when absent (p-props-correct.1), so <xs:sequence minOccurs="2"> (2 > the default 1) and <xs:all maxOccurs="0"> (the default 1 > 0) are rejected; and an all group's maxOccurs must be 1 and minOccurs 0 or 1, with every particle it contains limited to maxOccurs 0 or 1 (cos-all-limited.2). The previous check fired only when both attributes were present, missing the common default-maxOccurs cases. Triple-confirmed against the W3C-labelled suite and libxml2: invalid-schemas-accepted 130 → 110, with no false positive (valid-schemas-rejected held at 0) and no other bucket moved.

  • 2026-06-16

Second pre-release. Continues the W3C XSTS schema-validity campaign: invalid-schemas-accepted 394 → 266 (settled expectations), valid-instances-rejected 180 → 171, invalid-instances-accepted 158 → 155, valid-schemas-rejected held at 1 (particlesZ001, spec-ambiguous). Not yet 1.0: see docs/production-readiness.md.

Fixed

  • XSTS valid-schemas-rejected (#148) reaches 0: FALSE POSITIVES ARE NOW 0 (both buckets). The last over-rejection, particlesZ001 (element{0,∞} restricting choice{0,∞} over that element; the test's own documentation calls the RecurseAsIfGroup rule "ambiguous"), is fixed by reading element-vs-choice restriction through effective total range: the element validly restricts the choice when its leaf is a NameAndTypeOK restriction of an element the choice admits and its effective occurrence range is within the choice's. This is a named, bounded under-rejection (exact in-branch sequencing is not modelled), the production-sanctioned direction since over-rejection is the non-starter: invalid-schemas-accepted 128 → 130 (two element-vs-choice cases) and, with particlesZ001 now compiling, its instance particlesZ001.i (a previously-masked invalid-instance gap) is accepted, 132 → 133. Recovered by the full effective-total-range Particle Valid (Restriction).

  • XSTS valid-instances-rejected (#146) reaches 0. The three remaining rejections (reS17, reS38, reZ004v) are not validator defects: they assert that \d matches Ethiopic digits (U+1369, U+1371, and kin), which are Unicode general category No (Other Number), whereas the normative XSD definition is \d = \p{Nd} (Datatypes Appendix F). Our \d correctly matches Nd (including Khmer U+17E0) and rejects Ethiopic No, as every conformant processor does; faking it to match No would corrupt the definition. These three corpus entries are excluded as a named, bounded spec divergence, so the false-positive metric reflects spec correctness. valid-instances-rejected 3 → 0.

  • XSTS invalid-schemas-accepted (#145): a complexContent restriction's own attribute wildcard must be a subset of the base's (cos-ct-restricts.4). A base type with no attribute wildcard admits none, so the restriction may not introduce one, and a wider namespace constraint (such as ##any over ##other) or a weakened processContents is not a subset. Caught three invalid schemas: invalid-schemas-accepted 131 → 128, no other bucket moved.

  • XSTS invalid-schemas-accepted (#145): a complexContent restriction may not add an attribute the base type never permitted. An attribute the restriction declares that has no matching base attribute use must be admitted by the base's anyAttribute wildcard (cos-ct-restricts.3); for example a no-namespace attribute is not admitted by a ##other wildcard. A prohibited declaration (which removes an attribute) stays valid. Caught one invalid schema: invalid-schemas-accepted 132 → 131, no other bucket moved.

  • XSTS invalid-schemas-accepted (#145): a namespace value may not be the empty string. <schema targetNamespace=""> and <import namespace=""> are rejected, since a namespace name must be a non-empty anyURI (omitting the attribute, for no target namespace or a no-namespace import, stays valid). Caught two invalid schemas: invalid-schemas-accepted 134 → 132, no other bucket moved.

  • XSTS invalid-schemas-accepted (#145): a wildcard restriction may not weaken processContents. When a complex type restricts another, a restricting xs:any must have a processContents at least as strong as the base wildcard's (strict > lax > skip; Wildcard Subset, §3.10.6), in addition to narrowing the namespace. Caught three invalid schemas: invalid-schemas-accepted 137 → 134, no other bucket moved.

  • XSTS invalid-schemas-accepted (#145): a simpleContent restriction's base must be a complex type. Restricting a built-in (xs:integer, xs:anySimpleType) or a user simpleType through <simpleContent><restriction> is rejected (src-ct.2); a complex type with simple content is built by extending a simple type, not by restricting one. Caught five invalid schemas: invalid-schemas-accepted 142 → 137, no other bucket moved.

  • XSTS invalid-schemas-accepted (#145): Particle Valid (Restriction) now enforces the NameAndTypeOK nillable clause. A restricting element may not be nillable unless the base element it restricts is also nillable. Caught two invalid schemas: invalid-schemas-accepted 144 → 142, no other bucket moved.

  • XSTS invalid-schemas-accepted (#145): Particle Valid (Restriction) now enforces the NameAndTypeOK fixed-value clause. When the base element has a fixed value constraint, the restricting element must also be fixed to the same value (compared in the base type's value space, so a value-equal pair in different lexical forms is not wrongly flagged). Caught four invalid schemas: invalid-schemas-accepted 148 → 144, no other bucket moved.

  • XSTS invalid-schemas-accepted (#145): Particle Valid (Restriction) now also covers anonymous complex types. The named-type subset check was keyed by type name, so an inline complexType deriving by complexContent restriction (the common <element><complexType><complexContent><restriction base="..."> shape) was never validated against its base. Such restrictions are now compiled and checked with the same algorithm. Caught fifteen invalid schemas: invalid-schemas-accepted 163 → 148, no other bucket moved.

  • XSTS invalid-schemas-accepted (#145): Particle Valid (Restriction) now enforces the NameAndTypeOK block-superset clause. A restricting element's {disallowed substitutions} (its block, or the schema's blockDefault) must be a superset of the base element's, so a restriction may block more substitutions but never fewer. The element particle now carries this block set. Caught sixteen invalid schemas: invalid-schemas-accepted 179 → 163, no other bucket moved.

  • XSTS invalid-schemas-accepted (#145): the RecurseAsIfGroup case of Particle Valid (Restriction) now checks occurrence. An element restricting a base model group is treated as a synthetic group with minOccurs=maxOccurs=1 holding the element (XSD 1.0 §3.9.6), so a base group that must occur two or more times cannot be restricted to a single element. The element's own occurrence is still matched against the base branch it maps to, so an optional element restricting a once-or-more base group stays valid (no false rejection). Caught two invalid schemas: invalid-schemas-accepted 181 → 179, no other bucket moved.

  • XSTS invalid-schemas-accepted (#145): a redefine is now checked against the schema it redefines once that schema is loaded: every component the redefine names (a complexType, simpleType, group, or attributeGroup) must have a same-kind component of the same name in the redefined schema (src-redefine.6/7.2.1), and the same component may not be redefined twice in one redefine (src-redefine.7.2.2). A redefinition naming a component that does not exist there, or a duplicate redefinition, is rejected. The check runs only when the redefined document was loaded through a schemaLoader, so a stand-alone schema is unaffected. Caught two invalid schemas: invalid-schemas-accepted 183 → 181, no other bucket moved.

  • XSTS invalid-schemas-accepted (#145): an element or attribute default/fixed value constraint is now validated against an inline type (an inline simpleType, or an inline complex type with simpleContent) just as it already was against a named type reference, so a non-numeric default on a decimal-based inline type is rejected. Validated against the simpleContent base's value space, so only a clear value-space mismatch is flagged. Caught two invalid schemas: invalid-schemas-accepted 185 → 183, no other bucket moved.

  • XSTS invalid-schemas-accepted (#145): a complex type without its own final attribute now inherits the schema's finalDefault as its {final} property, so finalDefault="#all" or "restriction"/"extension" blocks a complexContent restriction or extension derived from it just as an explicit final would (an explicit final="" overrides the default back to no restriction). Complex types previously honoured only an explicit final. Caught four invalid schemas: invalid-schemas-accepted 189 → 185, no other bucket moved.

  • XSTS invalid-schemas-accepted (#145): the global-only element properties abstract, final, and substitutionGroup are now rejected on a local element declaration (an element particle, not a direct child of schema). They belong to the topLevelElement form only, so their presence on a local element is invalid at any value (abstract="false" is rejected as well as "true"). Caught five invalid schemas: invalid-schemas-accepted 194 → 189, no other bucket moved.

  • XSTS invalid-schemas-accepted (#145): a complexContent extension that explicitly states mixed must agree with its base type's mixedness (cos-ct-extends.1.4.3.2.2.1): a mixed extension may extend only a mixed base, and an element-only extension only an element-only base. An extension that omits mixed inherits the base's setting and stays valid, so only an explicit conflict is rejected. Caught three invalid schemas: invalid-schemas-accepted 197 → 194, no other bucket moved.

  • XSTS invalid-schemas-accepted (#145): a complexContent or simpleContent must contain exactly one restriction or extension. An empty <complexContent/> (or <simpleContent/>), which declares no derivation, is now rejected as a structural error. Caught three invalid schemas: invalid-schemas-accepted 200 → 197, no other bucket moved.

  • XSTS invalid-schemas-accepted (#145): a pattern facet whose \p{...} or \P{...} property name can be no XSD charProp (IsCategory | IsBlock) under any repertoire is now rejected as a schema-validity error: an empty name, the bare prefix Is, a non-block name carrying a non-letter (\p{\L}), or Is followed by a non-block character. A well-formed but unrecognised category or block name (such as an unknown Is-block) stays a lenient engine limitation rather than an error, so only the unambiguously malformed forms reject. Caught three invalid schemas: invalid-schemas-accepted 203 → 200, no other bucket moved.

  • XSTS invalid-schemas-accepted (#145): two attribute-declaration validity rules. An attribute ref may not also carry form (the form comes from the referenced global declaration; src-attribute.3.2), and no attribute declaration may be named xmlns (that name is reserved for namespace declarations; no-xmlns). Caught four invalid schemas: invalid-schemas-accepted 207 → 203, no other bucket moved.

  • XSTS invalid-schemas-accepted (#145): a referencing attributeGroup (one carrying a ref) is held to its (annotation?) content model. It names a definition elsewhere, so it may not itself declare attributes, nested attributeGroup references, or an anyAttribute; only a defining attributeGroup (with name) carries those (attgD). Caught three invalid schemas: invalid-schemas-accepted 210 → 207, no other bucket moved.

  • XSTS invalid-schemas-accepted (#145): notation names are now part of the component-name uniqueness check. A notation has its own symbol space, so two notations sharing a name and target namespace are a duplicate just as two global types or elements would be (notatB005). Caught one invalid schema: invalid-schemas-accepted 211 → 210, no other bucket moved.

  • XSTS invalid-schemas-accepted (#145): an xsd:notation declaration is held to its (annotation?) content model. A notation may carry only an optional annotation child, so any other element child, or any non-whitespace character content, is now rejected (notatG). Caught three invalid schemas: invalid-schemas-accepted 214 → 211, no other bucket moved.

  • XSTS invalid-schemas-accepted (#145): a list simple type must draw its item type from exactly one of an itemType attribute or an inline simpleType child (neither, or both, is invalid), and a union must declare at least one member type through a memberTypes attribute or an inline simpleType child (an empty union declares none), per src-simple-type.2/.3. Caught one invalid schema: invalid-schemas-accepted 215 → 214, no other bucket moved.

  • XSTS invalid-schemas-accepted (#145): a simple type without its own final attribute now inherits the schema's finalDefault as its {final} property (XSD 1.0 Schemas §3.14.2), so finalDefault="list" or "union" blocks the matching list-item or union-member derivation just as an explicit final would. An explicit final="" still overrides the default back to no restriction. Caught two invalid schemas: invalid-schemas-accepted 217 → 215, no other bucket moved.

  • XSTS invalid-schemas-accepted (#145): a pattern facet whose regular expression ends mid-token is now rejected as a schema-validity error. A trailing \ with no character to escape (a\) raises RegexError.incompleteEscape, and a [ character class the expression ends before closing with ] (a[, [a[:xyz:) raises RegexError.unterminatedClass; both are unambiguously invalid rather than an engine limitation on an otherwise-valid pattern, so they reject. Caught thirteen invalid schemas: invalid-schemas-accepted 230 → 217, no other bucket moved.

  • XSTS invalid-schemas-accepted (#145): an attribute use that references a global attribute carrying a fixed value constraint may not contradict it. A fixed on the reference whose value differs from the global's (compared in the attribute type's value space, so 1 and 01 are not wrongly flagged), or a default on a reference to a fixed global, is invalid (au-props-correct.2). A reference that omits its own value constraint inherits the global's fixed value and stays valid. Caught two invalid schemas: invalid-schemas-accepted 232 → 230, no other bucket moved.

  • XSTS invalid-schemas-accepted (#145): a complexContent extension that adds element content may not extend a base whose whole content is a non-empty all group; the base's all would be nested in the sequence joining base and extension content (cos-all-limited). An empty-all base, or an attribute-only extension, stays valid. Caught one invalid schema: invalid-schemas-accepted 233 → 232, no other bucket moved.

  • XSTS invalid-schemas-accepted (#145): a complexContent extension that adds element content (a non-empty model group) may not extend a base whose content is simple (cos-ct-extends.1.4.2.2); a simple-content type is extended through simpleContent. An attribute-only complexContent extension of a simpleContent base stays valid (the complexContent-around-simpleContent idiom). Caught two invalid schemas: invalid-schemas-accepted 235 → 233, no other bucket moved.

  • XSTS invalid-schemas-accepted (#145): two more schema-for-schemas structural rules. A named model group's compositor (all/choice/sequence) may not carry minOccurs/maxOccurs (those belong on a group reference), and an attribute in the XML Schema namespace (such as xsd:type) is rejected since the schema vocabulary defines no namespaced attributes (a foreign attribute in any other namespace stays the author's own). Caught nine invalid schemas: invalid-schemas-accepted 244 → 235, no other bucket moved.

  • XSTS invalid-schemas-accepted (#145): unambiguously malformed regular expressions in pattern facets are now schema-invalid: a reversed character-class range ([b-a]), an empty class ([]), and a reversed bounded quantifier ({37,17}). The parser raises distinct errors for these, which the schema-validity check rejects, while constructs the engine merely does not support (an untabulated \p{Is…} block, the lenient {,m} form) stay non-rejecting so a valid pattern is never refused. Caught eighteen invalid schemas: invalid-schemas-accepted 262 → 244, no other bucket moved.

  • XSTS invalid-instances-accepted (#147): identity constraints reject more invalid instances. Fields now enforce cardinality (a field must select at most one node; cvc-identity-constraint.3), so an instance whose unique/key/keyref field matches several nodes is rejected. Selector and field XPath prefixes now resolve against the schema's namespace context (where the constraint is declared) rather than the instance document, so a constraint like .//v:vehicle works whatever prefix the instance uses; a QName field value still compares using the instance's bindings. An attribute field's type also resolves through the selector's target global element, so values compare in their value space (3.0 and 3 are the same xsd:decimal key). Together these caught fifteen cases (idF005, idG005, idF010, the idc004/5/6 cluster, idZ010, and kin): invalid-instances-accepted 147 → 132, no other bucket moved.

  • XSTS valid-instances-rejected (#146): 173 → 3 on the 2006-11-06 archive (settled-status count; the four buckets now read 1/262/3/143). Regex patterns match Unicode scalars (not Swift grapheme clusters), fixing \p{Is…} block tests and combining-mark cases; empty patterns match only the empty string. The regex repertoire is pinned to Unicode 3.2, the version the corpus is consistent at: code points added in 3.2 (Tagalog, Cyrillic Komi) are word characters, while later additions (the Tamil digit zero) stay unassigned and match \D (XSDCategory, Unicode32Assigned). Per-child content assessment is driven by the content-model particle the automaton matched, not a by-name lookup (#180), so two same-named particles keep their own fixed values and an element matching a skip wildcard is not validated against its global declaration. Schema documents that form an import chain are compiled as a union rather than merged as separately compiled documents, so cross-document substitution groups resolve (#161, Document.init(composing:)). Plus attribute-group anyAttribute inheritance, extension wildcard union, namespace-qualified type tables for multi-document xsi:schemaLocation, per-container elementFormDefault, strict/lax attribute wildcard validation, built-in type resolution, and xs:redefine simpleType overrides.

  • Enforce element, attribute, attributeGroup, and group child-content shape (schema-for-schemas cardinality and order): at most one inline type per element/attribute, inline type before identity constraints, at most one compositor per group, at most one anyAttribute per attributeGroup with attribute references preceding it (SchemaStructureHelpers.swift).

  • Enforce facet applicability per base type, fractionDigits constraints on integer types, ordered duration range bounds, and built-in list non-emptiness constraints (XSD 1.0 Datatypes §4.3). Ensure that built-in list types (xs:IDREFS, xs:ENTITIES, xs:NMTOKENS) are validated as non-empty unless restricted with an explicit minLength. Resolve type references namespace-aware to avoid prefix conflation with user types of the same name. Ignore foreign elements matching facet names inside xs:restriction. XSTS: invalid schemas accepted 296 to 266, no other bucket raised.

  • Enforce schema element order (imports, includes, and redefines must precede other global declarations) and validate import constraints (namespace attribute must not match the schema's targetNamespace, and if missing, the schema must have a targetNamespace) (XSD 1.0 Structures §3.10.6, §3.15.2). XSTS: invalid schemas accepted 302 to 296, no other bucket raised.

  • Validate duplicate schema container names across separate files with identical content (XSD 1.0 Structures §3.14). Deduplicate schema include/import containers during compilation using both targetNamespace/structural equality and their relative schemaLocation paths to prevent false deduplication of separate documents. Keep circular self-imports/includes of the root schema valid. XSTS: invalid schemas accepted 304 to 302, no other bucket raised.

  • Validate standard xml:lang and xml:space attributes on XSD schema elements (like <xs:documentation> and <xs:appinfo>). Traversal is adjusted to process attributes on these documentation-related elements before skipping their foreign children, and validation checks the lexical pattern of xml:lang (using BCP 47) and restricts xml:space to 'default' or 'preserve'. XSTS: invalid schemas accepted 306 to 304, no other bucket raised.

  • Validate notation declarations and notation enumeration constraints on simpleTypes (XSD 1.0 Structures §3.12 / §3.14). Every <xs:notation> declaration must specify at least one of public or system attributes. For any simpleType restricting xs:NOTATION, all enumeration values must name a declared notation in the schema. Resolved prefix bindings are traversed recursively up the ancestor path to handle default namespaces and local namespace declarations. XSTS: invalid schemas accepted 309 to 306, no other bucket raised.

  • Enforce cross-container global component-name and identity-constraint uniqueness constraints (XSD 1.0 Structures §3.14). Within a schema (including all included and imported schemas), global type names (simpleType and complexType sharing one symbol space), global element names, global attribute names, named model groups, and named attribute groups must be unique within their respective symbol spaces and target namespaces. Identity constraints (unique, key, keyref) must have unique names across all target namespaces in the entire schema document. Circular and duplicate imports/includes are handled namespace-aware without causing false-positive validations on valid schemas. XSTS: invalid schemas accepted 316 to 309, no other bucket raised.

  • Enforce substitution group exclusions based on element final and schema finalDefault attributes (XSD 1.0 Structures §3.3.6 / Schema Component Constraint: Substitution Group OK (Transitive)), including support for element declarations with inline anonymous types. The entire derivation path from the member type to the head type is checked; if any step contains a derivation method prohibited by the head element's final or the schema's finalDefault, the substitution is rejected. XSTS: invalid schemas accepted 323 to 316, no other bucket raised.

  • Enforce targetNamespace scope matching on local schema references (XSD 1.0 Structures §3.14). If a schema has a targetNamespace declared, any local type, element, attribute, group, or attributeGroup reference without a namespace prefix (resolving to no namespace) is rejected as undeclared unless a default namespace matching the targetNamespace is declared on the element, since all global components defined in the document belong to the targetNamespace. XSTS: invalid schemas accepted 325 to 323, no other bucket raised.

  • Enforce variety constraints on list itemType and union memberTypes (XSD 1.0 Datatypes §3.3.1 / §3.4.1). A list's itemType must be atomic or union of atomic types, and cannot be a list or a union containing a list, and cannot resolve to 'anySimpleType'. Resolves namespace prefixes dynamically in scope to handle local prefix declarations without false positives. XSTS: invalid schemas accepted 327 to 325, no other bucket raised.

  • Implement namespace-aware schema reference resolution checks, validating type references, element/attribute/group references, and substitutionGroup headers using XML namespace prefix bindings (XSD 1.0 Structures §3.14). If a prefix is bound to a namespace that is neither the targetNamespace nor the XMLSchema namespace (when no external schemas are loaded), the reference is flagged as undeclared. Bypasses built-in validation checks on references in the XMLSchema namespace when the targetNamespace is the XMLSchema namespace itself. Validate simpleContent base restrictions namespace-aware. XSTS: invalid schemas accepted 333 to 327, no other bucket raised.

  • Enforce simpleType restriction structure and prefix binding constraints (XSD 1.0 Datatypes §3.2.1 / Structures §3.3.1 / §3.14.3). Verify that restriction under simpleType only admits annotation, simpleType and facets, disallowing attributes, wildcards, model groups, and multiple or misplaced inline simpleTypes. Reject a restriction base, list itemType, or union memberType resolving to a complex type (e.g. anyType). Enforce that all QName attributes have bound prefixes. XSTS: invalid schemas accepted 350 to 333, no other bucket raised.

  • A top-level group or attributeGroup definition may no longer carry local-use attributes (XSD 1.0 Structures §3.6.1 / §3.6.2). A global group definition (including one defined inside xs:redefine) must only have name and id attributes, and local-use attributes like ref, minOccurs, or maxOccurs are rejected; a global attributeGroup definition may not carry a ref attribute. XSTS: invalid schemas accepted 354 to 350, no other bucket raised.

  • Support attribute inheritance and prohibition in complexContent restriction derivations (XSD 1.0 Structures §3.4.6 / Schema Component Constraint: Derivation Valid (Restriction, Complex)). When a complexType restricts a base type, the restricted type inherits all base attributes that are not explicitly overridden or prohibited by the restriction. A prohibited attribute is excluded from the final type's attributes, preventing its use in instance validation. Resolves referenced attributes namespace-aware using prefix bindings. XSTS: valid instances rejected 180 to 171, invalid instances accepted 158 to 155, no other bucket raised.

  • A complexContent restriction must keep a base attribute's required use and its fixed value constraint (XSD 1.0 cos-ct-derived-ok / derivation-ok-restriction.2 / derivation-ok-restriction.4). A restriction redeclaring a base required attribute may not make it optional or prohibited, and if a base attribute use has a fixed value constraint, the restriction must also make it fixed with the same value. The fixed values are compared in the attribute type's value space (supporting lists and unions) to avoid over-rejecting valid schemas. A prohibited attribute is removed, so the fixed clause does not apply to it. XSTS: invalid schemas accepted 390 to 387, no other bucket raised.

  • Enforce top-level declaration naming, block xs:anySimpleType derivation, and restrict simpleType facets. Top-level definitions must specify a name attribute. Derivation by restriction or extension of xs:anySimpleType is disallowed. Restriction facets (length, minLength, maxLength, totalDigits, fractionDigits) must be tighter than or equal to the base type's facets. XSTS: invalid schemas accepted 387 to 383, no other bucket raised.

  • Enforce simpleType derivation constraints on restricted complexType attributes (XSD 1.0 cos-ct-derived-ok / derivation-ok-restriction.1). The type of a restricted attribute must be validly derived by restriction from its base counterpart, checking atomic built-in derivation, list item type restriction, and union membership or member-to-member restriction recursively. XSTS: invalid schemas accepted 367 to 365, no other bucket raised.

  • Enforce element type restriction constraints on restricted complexType particles (XSD 1.0 NameAndTypeOK / elementTypeRestrictionOK). Verify that the restricted particle's type validly derives by restriction from the base particle's type, checking union variety members structurally, prohibiting complex-restricting-simple and simple-restricting-complex (except where base is anyType), and falling back to name-based derivation. Prohibited attributes are exempted from type and value constraint checks. XSTS: invalid schemas accepted 365 to 357, no other bucket raised.

  • Enforce identity constraint keyref arity validation at schema compile time (XSD 1.0 c-identity-constraint). Verify that the number of fields in a keyref matches the arity of its referenced key or unique constraint. XSTS: invalid schemas accepted 357 to 354, no other bucket raised.

[0.1.0] - 2026-06-15

First tagged pre-release. A pure-Swift XML parser/emitter with an XSD 1.0 schema engine, XPath/XSLT support, and a WASI-compatible build. The W3C XSTS conformance campaign has driven invalid-schemas-accepted from 2461 to 394 with valid-schemas-rejected held at its floor of 1. Not yet 1.0: see docs/production-readiness.md for the remaining gates.

Added

  • A generative robustness fuzz harness for the schema engine and parser (#167, production-readiness stopper 4). A reproducible seeded generator builds structurally varied XSD schemas and XML instances that deliberately reach the crash- and hang-prone shapes (deep nesting, large/unbounded/inverted occurrences, pointless maxOccurs="0" particles, wildcards, element and group reference cycles, substitution-group chains, simpleType restriction cycles, complexContent restrictions, pathological regex patterns), and asserts the validator never crashes and always terminates. It is a regression gate against interactive-safety failures on hostile input, not a verdict-correctness oracle; counts are sized to stay fast on every swift test (the native .timeLimit hang guard is omitted under the single-threaded WASI runtime). No crashes or hangs were found over the current corpus.

Fixed

  • A nested simpleType or attributeGroup may no longer declare a name (XSD 1.0 schema-for-schemas localSimpleType / nested attributeGroup, #170). A simpleType inside a restriction/list/union/element/attribute is anonymous (the local form carries no name), and a nested attributeGroup is always a reference, never a named definition; named definitions are top-level only (a direct child of schema or redefine). Such schemas were accepted. A nested anonymous simpleType, an attributeGroup reference, and a top-level named definition are unaffected. XSTS: invalid schemas accepted 403 to 394, no other bucket raised.

  • An xs:all group may no longer appear in a complexContent extension of a base that already has element content (XSD 1.0 cos-all-limited.1.2, #170). The effective content model of such an extension is sequence(base content, extension content), which nests the all and violates the rule that an all must be the whole content model. Detected from the compiled base type: rejected only when the base resolves (in the schema's own target namespace) to a complex type with element-only or mixed content. An explicitly-empty base (an empty model group with mixed="false", whose content is effectively empty) may still be extended by an all, since the all becomes the whole content; a simple-content, empty, foreign, or unresolved base is left alone. Checked for a self-contained schema. XSTS: invalid schemas accepted 411 to 403, no other bucket raised.

  • A simple type whose final contains list (or #all) may no longer be used as a list itemType, and one whose final contains union (or #all) may no longer be a union memberType (XSD 1.0 simple-type final, #170). The restriction and extension directions were already enforced by finalRespected; the list and union directions were unchecked because final="list"/"union" is not modelled by the derivation-method set, so the final attribute is read directly. The reference is matched only when it resolves to the schema's own target namespace, so a list or union of a built-in (whose final is empty) is not confused with a user type of the same local name. Checked for a self-contained schema. XSTS: invalid schemas accepted 413 to 411, no other bucket raised.

  • A whiteSpace facet may only strengthen, never relax, its base type's effective whiteSpace (XSD Part 2 §4.3.6, #170). The order is preserve < replace < collapse, so a base fixed to collapse admits only collapse, and a replace base admits replace or collapse. A derivation that relaxes it (for example whiteSpace="replace" over a collapse base, or restricting xs:token to replace) was accepted and is now rejected. The base's effective whiteSpace is its own facet, falling back to its built-in intrinsic; the check is atomic-only and, when a base type's own whiteSpace has not yet resolved, can only under-detect, never reject a valid restriction. XSTS: invalid schemas accepted 422 to 413, no other bucket raised.

  • A top-level xs:element declaration may no longer carry ref, form, minOccurs, or maxOccurs (XSD 1.0 schema-for-schemas topLevelElement, #170). A global element is a declaration, never a particle or a reference, so those local-use attributes are invalid. The top-level-attribute applicability scan added earlier is generalized to cover both kinds; a local element particle inside a content model is unaffected. XSTS: invalid schemas accepted 424 to 422, no other bucket raised.

  • An element ref may no longer carry nillable, default, fixed, form, or block, and an element or attribute ref may no longer have an inline type definition (XSD 1.0 src-element.2.2, #170). A reference is a use of a global declaration, not a declaration, so beyond ref it carries only minOccurs, maxOccurs, and id; the applicability check already excluded name and type from a ref, and this extends the exclusion to the remaining declaration-only properties. A group/attributeGroup ref and an attribute ref's use/default/fixed (which override on the use) are unaffected. XSTS: invalid schemas accepted 439 to 424, no other bucket raised. Disclosed under-detection: a key/unique/keyref child under a ref is not yet flagged.

  • A top-level xs:attribute declaration may no longer carry use, form, or ref (XSD 1.0 schema-for-schemas topLevelAttribute, #170). Those belong to a local attribute use, not a global declaration; the per-component applicability table is the permissive global/local union and so could not draw the distinction, leaving such schemas accepted. A focused scan of the direct attribute children of schema (and of redefine) supplies the global-only constraint; a local attribute use inside a complex type or attribute group is unaffected. XSTS: invalid schemas accepted 454 to 439, no other bucket raised.

  • A default/fixed value on an element or attribute whose type names a user-declared type must now be a valid value of that type (XSD 1.0 e-props-correct.2 / a-props-correct.2, #170). This runs after the named types are compiled, complementing the existing built-in-only check: a default failing the type's pattern, a malformed value for a type restricting a built-in, or a value invalid against a complex type's simple content is now rejected. The type name is resolved namespace-aware; only a type in the schema's own target namespace is checked (built-ins are handled at structure time; imported, foreign, or inline anonymous types are left alone), and the compiled SimpleType validator (the one instance validation uses) performs the check, so whitespace and facet handling match. XSTS: invalid schemas accepted 462 to 454, no other bucket raised.

  • A simple-type restriction whose base is an inline anonymous <simpleType> (no base attribute) now resolves that inline child as its base, preserving its list or union variety. The base was read only from the base attribute, so an inline list/union base was dropped and the type collapsed to an atomic string: a length facet then counted characters instead of list items, and a list-valued default of the correct length was wrongly rejected once value constraints are checked. Resolving the inline base fixes both schema-time value checking and instance validation. XSTS: invalid instances accepted 160 to 158, no other bucket raised.

  • An xs:attribute declaration carrying a default value constraint must now have use="optional" (XSD 1.0 src-attribute.2, #170). A default makes the attribute optional, so use="required" or use="prohibited" alongside it is contradictory; such schemas were accepted. The check rides the existing value-constraint walk, gated to XSD-namespace attribute declarations reading the unprefixed default/use, and is per declaration. Only fixed (not default) may pair with use="required". XSTS: invalid schemas accepted 469 to 462, no other bucket raised. Disclosed under-rejection: a whitespace-padded use=" required " (whose NMTOKEN actual value is required) is not flagged.

  • An element or attribute declaration may no longer carry both a default and a fixed value constraint (XSD 1.0 src-element.1 / src-attribute.1, #170). The two are mutually exclusive; such schemas were accepted. The check rides the existing structure walk, gated to XSD-namespace declarations and reading the unprefixed default/fixed (a foreign same-local-name attribute is not the value constraint), and is per declaration, so a default on a global declaration and a fixed on a reference to it (two separate items) is correctly allowed. XSTS: invalid schemas accepted 479 to 469, no other bucket raised.

  • A substitution-group member's type must now be validly derived from the type of its head (XSD 1.0 e-props-correct.4, #170). An element declaring substitutionGroup="head" whose own type is unrelated to the head's (for example a member typed xs:int under a head typed xs:string) was accepted; it is now rejected. The check runs after named types resolve and reuses the type-derivation oracle (the restriction/extension chain, continued through the built-in lattice; an ur-type head admits any member). To never reject a valid member it is conservative: it runs only for a self-contained schema (no import/include/redefine, where the derivation table is complete and single-namespace names are unambiguous), reads the member's type from its own type attribute and the head's from the top-level element declarations only (so a local element of the same name supplies neither), and stands down when either type is a list or union (the cos-st-derived-ok clauses 2.3/2.4 union-member derivation is not modelled). XSTS: invalid schemas accepted 480 to 479, no other bucket raised. Disclosed under-rejections: schemas that compose other documents, list/union-typed members or heads, and inline/untyped members.

  • An xs:all group referenced through a <group ref> from inside a compositor is now rejected (XSD 1.0 cos-all-limited.1.2, #170): an all group may only be the whole content model of a complex type. A direct nested all was already rejected by the child-content table; this closes the group-reference path. The all-group pool is qualified by the defining schema's target namespace and a reference by its prefix, so an imported or redefined same-named group is not confused with a local one; a group redefined to a non-all content is exempt. XSTS: invalid schemas accepted 481 to 480, no other bucket raised.

  • An attribute or element whose type is or derives from xs:ID may no longer carry a default or fixed value constraint (XSD 1.0 a-props-correct.3 / e-props-correct.5, #170). Such schemas were accepted. The check runs after named simple types resolve, so a named user type derived from xs:ID is recognized; the type reference's namespace is resolved from its prefix, so a user type named ID or an imported foo:ID is not mistaken for the built-in. XSTS: invalid schemas accepted 489 to 481, no other bucket raised.

  • A type attribute and an inline anonymous type definition are now mutually exclusive on an element or attribute (XSD 1.0 src-element.3 / src-attribute.3.1, #170). An <xs:element> or <xs:attribute> carrying both a type= and an inline <simpleType> (or <complexType>, for an element) is rejected; previously it was accepted. Only an inline type in the XSD namespace counts, so a foreign-namespace child that happens to be named complexType/simpleType is not mistaken for the component's own type. XSTS: invalid schemas accepted 495 to 489, no other bucket raised.

  • Attribute-use uniqueness and the single-ID rule are now enforced (XSD 1.0 ct-props-correct.4 / ag-props-correct.2 / ct-props-correct.5, #162). A complex type's or attribute group's attribute-use set (flattened through attributeGroup references) may not contain two distinct declarations with the same name and namespace, nor more than one attribute whose type is or derives from xs:ID. Declarations are deduplicated by the identity of their declaring node, so the same declaration reached by two paths (a diamond of attribute-group references) is one use, not a clash; names carry their resolved namespace, so a local attribute and a reference to a same-local-name attribute in an imported namespace do not collide. XSTS: invalid schemas accepted 505 to 495, no other bucket raised. Disclosed under-rejections: the set inherited through extension is not composed in, and a duplicate that requires an unprefixed reference in a schema with no default namespace is not detected.

  • Circular references among model groups, attribute groups, and substitution-group affiliations are now rejected (XSD 1.0 mg-props-correct.2, ag-props-correct.3, e-props-correct.6, #168). A model group may not contain itself, an attribute group may not reference itself transitively, and a substitution chain may not loop. The model-group check follows references only within a definition's own compositor tree, stopping at element and type boundaries: a group referenced inside an element's content is a recursive data structure (permitted), not the group containing itself. A reference is followed only when it resolves to the schema's own target namespace, and a group or attribute group redefined in xs:redefine (whose single self-reference is the legal redefinition) ends the chain. XSTS: invalid schemas accepted 516 to 505, no other bucket raised. A genuine cycle introduced across two redefined definitions is a disclosed under-rejection.

  • Circular type derivation is now rejected at compile time (XSD 1.0 ct-props-correct.3 / st-props-correct.2, #168). A complex or simple type may not derive, transitively through its base, from itself; previously such a schema was accepted. Only the base chain is followed, so a type whose element content recurses (an element of its own type, the normal recursive-data-structure case) is correctly permitted, and a type redefined in xs:redefine (whose base legitimately names its former self) and the ur-types (anyType/anySimpleType/anyAtomicType, the roots the schema-for-schemas bootstraps) end the chain rather than appear as cycles. The base is followed only when it resolves to the schema's own target namespace, so a type whose base is a same-local-name type in another namespace is not mistaken for a self-cycle. XSTS: invalid schemas accepted 519 to 516, no other bucket raised. Union/list (itemType/memberTypes) and genuine cross-namespace derivation cycles are a disclosed under-rejection (the engine resolves by local name).

  • Particle Valid (Restriction) now implements the W3C MapAndSum rule for a sequence restricting a choice (#165). When a derived sequence restricts a base choice, the spec (XSD 1.0 §3.9.6) requires that each derived particle restrict some base branch AND that the derived sequence's occurrence count times its number of particles lie within the base choice's occurrence range. PureXML previously applied the same-compositor occurrence subsumption (comparing the sequence's own range directly to the choice's), which both rejected a valid restriction (sequence{2,4}(e1,e2) of choice{3,9}(e1,e2), whose 4-to-8 elements fit the choice's 3-to-9) and accepted an invalid one (a two-member sequence restricting a single-occurrence choice, which the count-product 2 > 1 now rejects). The count-product is the spec's measure, matched to the reference implementation. XSTS: valid schemas rejected 2 to 1, invalid schemas accepted 520 to 519, no other bucket raised. The one remaining valid-rejected case (particlesZ001) is a documented spec ambiguity (its own test notes the RecurseAsIfGroup rule "is ambiguous"; the strict reading the reference follows rejects it), recorded in docs/xsts-deviations.md.

  • Two more Particle Valid (Restriction) over-rejections closed (#148). A derived particle that can never contribute a child (its own maxOccurs="0", or a group whose every member is content-free) accepts only the empty sequence, so it is now recognized as a valid restriction of any emptiable base before the structural cases run; previously a maxOccurs="0" group whose members carried their own non-trivial occurrences was wrongly recursed into and rejected (particlesW006). And substitution-group expansion now lists members in declaration order: the closure walk used a popLast() stack that reversed them, so a reference to a head expanded to choice(head, m2, m1) and an order-preserving RecurseLax restriction by choice(m1, m2) could not map (elemZ027a). The choice/choice restriction stays order-preserving, as the W3C suite requires (a choice(b|a) is not a valid restriction of choice(a|b), particlesT002); only the expansion order is corrected. The expansion is shared with instance validation and the determinism check, both order-neutral, so neither is affected. XSTS: valid schemas rejected 4 to 2, no other bucket raised. (The pinned XSTS baselines, which the opt-in suite never asserts under a plain swift test, were also corrected to the measured 2/520/180/160.)

  • An <xs:element ref="p:foo"> now resolves to the namespace its prefix is bound to, not the schema's target namespace (#164). The reference's QName is resolved by standard XML namespace rules against the schema root's xmlns: bindings: a bound prefix gives its namespace (so a reference to an imported element lands in the imported namespace), an unprefixed name resolves through the default namespace, and a name with no applicable binding is in no namespace. Previously the prefix was stripped and the element was assumed to be in the target namespace, which mis-qualified every reference to an imported element: a restriction of an ##other wildcard by such a reference was wrongly rejected (the element looked like it was in the target namespace, which ##other forbids), and the same mis-qualification rejected valid instance documents whose elements were correctly in the imported namespace. Because the content model is shared between schema-restriction checks and instance validation, the fix improves both. XSTS: valid schemas rejected 32 to 4, invalid schemas accepted 526 to 520, valid instances rejected 214 to 180, invalid instances accepted held at 160. Disclosed limitations, unchanged by this fix: a prefix re-bound on an inner element (rather than the schema root) is not honored, substitution-group members are still resolved to the target namespace (cross-namespace substitution is not modeled), and a dangling reference is not yet flagged as a schema error.

  • Particle Valid (Restriction) now derives its pairwise check over the normalized content model, and applies occurrence subsumption only where the two particles' own ranges are comparable (#163). The W3C restriction algorithm is defined over a content model with pointless particles eliminated, so both the derived and base particles are first normalized (maxOccurs="0" members dropped, a {1,1} single-member group unwrapped to its member, a nested same-compositor {1,1} group spliced into its parent), all language-preserving. The outer occurrence-range check (rangeSubsumed) was being applied to every pairing, including the cross-kind ones where it compares mismatched ranges: a derived element's range against a base group's, or a derived group's against a base wildcard's. It is now applied only to the same-kind pairings (element/element, wildcard/wildcard, group/group) and element/wildcard; for element/group (RecurseAsIfGroup), group/element, and group/wildcard the occurrence is matched inside the case, against a member or as an effective total range. This unblocks a single-member derived sequence narrowing an occurrence (sequence(e1{2,3}) restricting sequence(e1{1,3}, ...), the e1{2,3} no longer compared to the base sequence's own {1,1}). A group restricting a wildcard (NSRecurseCheckCardinality) is now valid when every leaf it can contain is admitted by the wildcard and the group's effective total occurrence range lies within the wildcard's; a group restricting a single element is bounded the same way, by that element's name, type derivation, and occurrence range. XSTS: valid schemas rejected 45 to 32 (13 correct schemas no longer wrongly rejected, the particlesHa family of element-restricts-wildcard cardinality among them), invalid schemas accepted 528 to 526 (the cardinality bound also catches two over-wide restrictions), no other bucket raised.

  • Particle Valid (Restriction) no longer over-rejects valid restrictions (#148). Three corrections, all language-preserving: a particle that can never occur (maxOccurs="0", a "pointless particle") is removed from both the derived and base content before mapping, so it no longer consumes a base particle and starves the ones after it (e.g. (e1 maxOccurs="0", e2) restricting (any) is now valid); a content-free restriction (an empty <sequence/>, or a group whose members all cannot occur) is a valid restriction of an EMPTY base, since both accept only the empty sequence; and a derived group that reduces to empty after that removal is a valid restriction of its base only when the base is itself emptiable (by structure or its own occurrence), closing a vacuous-acceptance gap the removal would otherwise open. XSTS: valid schemas rejected 72 to 45 (27 correct schemas no longer wrongly rejected), with one schema (addB32) moving to invalid-accepted because its only invalidity is a malformed imported document PureXML does not yet load (tracked by #161), not the restriction.

  • A simpleType must contain exactly one of restriction, list, or union (#158). Per XSD 1.0 Datatypes its content is (annotation?, (restriction | list | union)), but the membership table admitted each name without the cardinality, so an empty simpleType or one combining two derivations (two unions, a union and a list, two restrictions) was accepted (the stB family). The exactly-one rule is now enforced. XSTS: invalid schemas accepted 537 to 527, no other bucket raised (valid schemas rejected held at 72).

  • The final and block derivation-control attributes are now checked against their value space (#158). Each is #all or a whitespace list of method tokens, but the admitted tokens depend on the component: an element's final is extension/restriction and its block adds substitution; a complexType's final/block are extension/restriction; a simpleType's final is list/union/restriction. The tokens were parsed leniently (unknown tokens ignored, substitution admitted everywhere), so final="substitution" on an element, a misspelled or miscased token, or #all mixed with other tokens was accepted (the elemF/st_final families). The list is now validated against the component's exact set; finalDefault/blockDefault (the wider schema-level value space) and foreign attributes are left alone. XSTS: invalid schemas accepted 580 to 537, no other bucket raised (valid schemas rejected held at 72).

  • A simpleContent derivation's content is now checked for order and disallowed children (#158). A restriction is (annotation?, (simpleType?, facet*), (attribute | attributeGroup)*, anyAttribute?) and an extension is (annotation?, (attribute | attributeGroup)*, anyAttribute?): simpleContent has no model group, an extension has no facets or simpleType, and the attributes precede a single trailing anyAttribute. The membership table admitted the names but not the model, so a model group inside simpleContent, a facet in an extension, a facet before its simpleType, an attribute after anyAttribute, or a second anyAttribute (the ctD family) was accepted. The derivation's children are matched against the context's ordered slot model, reusing the slot walk built for complexContent. XSTS: invalid schemas accepted 594 to 580, no other bucket raised (valid schemas rejected held at 72).

  • Particle Valid (Restriction) now checks an element's type by derivation, not just its name (NameAndTypeOK, #158). When a restriction maps a same-named element onto a base element, the restricting type must be, or derive by restriction from, the base element's type; renaming xs:string to xs:int, widening a concrete type to xs:anyType, or naming an unrelated type (a different user type, a wider ancestor, an absent/anyType type over a concrete base) was accepted because only the names were compared. Each element particle now carries its type's derivation identity (the resolved type name, anyType for an absent type, or nil for an inline anonymous type), named simpleType restriction derivations are recorded alongside complex ones, and the check walks the user restriction chain and then the built-in lattice to decide derivation by name; an ur-type base, a union base (membership is not a restriction chain), and an inline anonymous type are permitted, so no valid restriction is rejected. XSTS: invalid schemas accepted 621 to 594, invalid instances accepted 165 to 160 (more invalid schemas rejected up front), no bucket raised (valid schemas rejected held at 72).

  • An element/attribute default/fixed value must be a valid value of its type (Attribute/Element Locally Valid, XSD 1.0, #158). A fixed="abc" on an xs:integer or a default="1 2" on an xs:int was accepted because the value constraint was never checked at compile time. The value is now validated against its type when the type resolves to a built-in datatype (namespace-aware: only a type in the XSD namespace), normalising whitespace per the type as XSD requires (so default=" 5 " on an integer is valid); a named or inline user type is left to its own rules, so no valid value constraint is rejected. XSTS: invalid schemas accepted 638 to 621, no other bucket raised (valid schemas rejected held at 72).

  • A constraining facet must apply to its simple type's variety (XSD 1.0 Part 2, #158). A list may be constrained only by length/minLength/maxLength, pattern, enumeration, and whiteSpace; a union only by pattern and enumeration. A value-bound facet (maxInclusive, totalDigits, ...) constrains an atomic value space, so restricting a list (list of integer) by maxInclusive was wrongly accepted. The base's variety is resolved from the raw tree (the check runs before the types are compiled), namespace-aware so a user type that shares a built-in's local name does not flip the variety: a base in the XSD namespace is a built-in (only the list datatypes are list-variety), a base in the schema's own target namespace resolves to a declared global, and an external or unresolvable base is skipped, so no valid restriction is rejected. XSTS: invalid schemas accepted 664 to 638, no other bucket raised (valid schemas rejected held at 72).

  • A complexType's shorthand content (without simpleContent/complexContent) is now checked for order, not just cardinality (#158). Its model is (annotation?, (group | all | choice | sequence)?, (attribute | attributeGroup)*, anyAttribute?): an optional model group, then attributes, then an optional anyAttribute. The existing content-shape check caught the wrong number of model groups and the exclusivity of a content spec, but not the order, so a model group after an attribute, an attribute after anyAttribute, or a second anyAttribute (the ctB family) was accepted. The complexType's direct children, excluding annotation and the content specs, are matched against the same ordered slot model used for a complexContent derivation. XSTS: invalid schemas accepted 687 to 664, no other bucket raised (valid schemas rejected held at 72).

  • An any/anyAttribute wildcard's namespace constraint is now validated at compile time (#158). Its value space is ('##any' | '##other') standing alone, or a list of namespace URIs and the ##targetNamespace/##local tokens; the wildcard parser mapped any unrecognised token to a literal URI, so a misspelled token (##all, ##target), an ##any/##other placed inside a list (##any ##other), or any other ##-prefixed token was accepted. The unprefixed namespace value is now tokenised and checked against that grammar, while ##any/##other are admitted only as the whole value; a prefixed (foreign) attribute that merely shares the local name is the author's own and is not checked, so no valid schema is rejected. XSTS: invalid schemas accepted 736 to 687, no other bucket raised (valid schemas rejected held at 72).

  • An identity constraint's selector/field xpath is now validated against the XSD 1.0 restricted-XPath subset at compile time (#158). That subset (Part 1, 3.11.6) is far narrower than full XPath: a path is an optional leading .// then /-separated steps, a step is ., a name test (QName, *, or NCName:*), or a child:: axis, and a field path may end in an attribute step (@x or attribute::x); there are no predicates ([...]), no other axes (self::, descendant::), no absolute or interior //. The xpath was stored raw and compiled lazily with try?, so a malformed one (a predicate, a bare /, an empty union branch, a spaced name-test colon) was silently accepted. It is now lexed into a whitespace-tolerant token stream and parsed against the grammar, so whitespace between tokens is allowed (child :: a, . //.) while // must be adjacent and a name test's own : may not be spaced; the name-test lexer uses the XML NameChar set, so a non-ASCII NCName is accepted. XSTS: invalid schemas accepted 785 to 736, no other bucket raised (valid schemas rejected held at 72).

  • A complexContent derivation's content is now checked for order and cardinality, not just child membership (#158). A restriction or extension inside complexContent has the content model (annotation?, (group | all | choice | sequence)?, (attribute | attributeGroup)*, anyAttribute?): an optional model group, then any number of attribute/attributeGroup, then an optional anyAttribute. The allowedChildren membership table admitted the right child names but not their sequence or count, so a second model group (sequence then sequence), a model group after an attribute, an attribute after anyAttribute, or a second anyAttribute was accepted. The children are matched against an ordered slot model (one model group, unbounded attributes, one anyAttribute) by a forward-only walk; the check runs only in the complexContent context, where a model group is part of the model, so a simpleContent derivation (facets, no model group) keeps its own rules and no valid schema is rejected. XSTS: invalid schemas accepted 864 to 785, no other bucket raised (valid schemas rejected held at 72).

  • Unique Particle Attribution (cos-nonambig, the content-model determinism constraint) is now enforced at compile time (#156, #157), the keystone of the remaining schema-validity tail. A content model must be deterministic so each element in an instance is attributable to a single particle without lookahead; an ambiguous model (<choice> of two same-name elements, an optional or repeating element followed by a distinct same-name element, two same-name members of an all, an element competing with a wildcard that admits it) was accepted. The check runs a Glushkov position automaton over a literal, QName-resolved view of the raw schema tree built specifically for this rule, independent of the instance-validation content model: a ref is one position carrying its prefix-resolved QName (not the blanket target namespace), and substitution groups are not expanded, which removes the two false-overlap sources (cross-namespace refs, expanded substitution members) that a check over the compiled model produced. Determinism is per particle, the W3C rule's defining property: a particle competing with a repetition of itself is never a violation, so a fixed count {n,n} unrolls into copies in sequence (b{2,2}, b stays the deterministic b, b, b) while a true range or unbounded repeat uses a self-loop, and only two distinct source particles with overlapping labels conflict. The overlap test is QName-only (not substitution-group-aware), an accepted under-rejection that can never over-reject. XSTS: invalid schemas accepted 884 to 864, no other bucket raised (valid schemas rejected held at 72).

  • A keyref's refer must name a key or unique constraint (#147). A refer pointing at a non-existent (or non-key/unique) constraint was accepted; it is now resolved against the document's key/unique names, with a forward refer (textually before the key) handled by collecting all names first. The check is skipped when the document pulls in external definitions through import/include/redefine, which the default compile does not load. XSTS: invalid schemas accepted 891 to 884, no other bucket raised.

  • Component-name uniqueness is now enforced (#147). Within a schema, global type names (simpleType and complexType share one symbol space), global element names, global attribute names, named model-group names, and named attribute-group names must each be unique, and identity-constraint names must be unique across the document; a duplicate was accepted, with the later definition silently overwriting the earlier. Only the document's own globals are examined (an xs:redefine's same-named type is not a clash), and the symbol spaces are kept separate (a type and an element may share a name). XSTS: invalid schemas accepted 909 to 891, no other bucket raised.

  • Element Declarations Consistent (cos-element-consistent) is now enforced (#147). Within one complex type's content model, two element declarations with the same name must have the same type; two same-name elements with different types (e.g. a as xs:string and as xs:integer) were accepted. The check walks the content model (through model groups and content-derivation wrappers) but not into an element's own nested type, so a reused name in a nested anonymous type, a separate content model, is not flagged. Modeled on Xerces (libxml2 only partially enforces this); substitution-group expansion is not folded in (an accepted under-rejection). XSTS: invalid schemas accepted 925 to 909, no other bucket raised.

  • Schema-vocabulary attribute applicability is now enforced at compile time (#147), the attribute sibling of the content-model table. Each XSD component admits a fixed set of attributes (the schema for schemas); an unprefixed attribute outside that set (a typo like nullable, a stray foo, name on a sequence) is now rejected, and ref may not co-occur with name or type. Foreign (prefixed) attributes and namespace declarations are not the component's own and are not checked, and the element/attribute sets are the permissive union of global and local use, so no valid schema is rejected. XSTS: invalid schemas accepted 956 to 925, no other bucket raised.

  • A named model-group definition (xs:group name="...") must contain exactly one of all/choice/sequence (#147). XSD 1.0 gives a group's content as (annotation?, (all | choice | sequence)), but the allowedChildren set-membership check did not enforce the cardinality, so a group with two compositors or with none was accepted. A group reference (no name) is excluded. XSTS: invalid schemas accepted 977 to 956, no other bucket raised.

  • A particle whose minOccurs exceeds its maxOccurs is now rejected at compile time (#147). An occurrence range like minOccurs="5" maxOccurs="2" is empty and illegal, but was accepted; the two values are compared by canonical magnitude (independent of machine-integer range), with unbounded never exceeded and a single-point range (minOccurs="2" maxOccurs="2") allowed. XSTS: invalid schemas accepted 997 to 977, no other bucket raised.

  • A complexType's content-model shape is now enforced at compile time (#147). XSD 1.0 allows a complexType's content to be simpleContent, or complexContent, or a model group followed by attributes, but never a mix: so at most one of simpleContent/complexContent, neither beside a model group or a direct attribute (those belong inside the derivation), and at most one model group. The allowedChildren set-membership check did not catch these combinations, so a malformed complexType (two simpleContent, simpleContent plus a sequence, two model groups) was accepted. Only the complexType's direct children are examined, so attributes legitimately nested inside an extension/restriction are not flagged. XSTS: invalid schemas accepted 1051 to 997, no other bucket raised.

  • Unresolvable references are now rejected at compile time (#147). Every QName a schema names (a type/base/itemType/memberTypes type, an element/attribute/group/attributeGroup ref, an element substitutionGroup) must resolve to a declared component or a built-in; an undeclared reference was previously accepted and only surfaced (if at all) lazily during instance validation. The reference is matched by local name with whitespace trimmed, as the rest of the compiler resolves it. The check is skipped when the document pulls in external definitions through import/include/redefine, which the default compile does not load, so a reference into them is never flagged. XSTS: invalid schemas accepted 1092 to 1051, no other bucket raised.

  • pattern facet regular expressions are now validated at compile time (#147). Each pattern value is compiled when the schema is; a structurally malformed one (unbalanced parentheses ((a), a quantifier with nothing to repeat ?a) is rejected, where before it was only compiled lazily at instance time (try?) and so silently ignored. The check is conservative: a construct the engine merely does not support on an otherwise-valid pattern (the empty pattern, an untabulated \p{Is...} block such as a surrogate or historic block, the lenient {,m} quantifier) is tolerated, so no valid schema is rejected. XSTS: invalid schemas accepted 1129 to 1092, no other bucket raised.

  • Schema-vocabulary name and reference attribute values are now checked for lexical validity at compile time (#147). A component's name must be a valid NCName (name="", name="123", name="a:b" are rejected), and the reference attributes type/base/ref/itemType/refer/substitutionGroup must be lexical QNames (type=":_", type="a:b:c" rejected), with memberTypes a list of QNames; these were unchecked and the schema accepted. Only the lexical form is checked (a QName's prefix is resolved elsewhere), so no valid schema is rejected. XSTS: invalid schemas accepted 1190 to 1129, no other bucket raised.

  • Schema-vocabulary attribute values are now validated at compile time (#147), the attribute-model sibling of the structural content-model check. Enumerated attributes (form/elementFormDefault/attributeFormDefault to qualified/unqualified, use to optional/prohibited/required, processContents to skip/lax/strict, mixed/abstract/nillable to a boolean) and minOccurs/maxOccurs (a nonNegativeInteger, maxOccurs also unbounded) are checked against their fixed value space; an out-of-space value (use="foo", mixed="yes", minOccurs="-1") was previously ignored and the schema accepted. Only unprefixed attributes on XSD-namespace elements are checked, so a foreign attribute is never flagged. XSTS: invalid schemas accepted 1313 to 1190, no other bucket raised.

  • Schema documents are now structurally validated against the schema-for-schemas content model (#147), the general mechanism for the structural tail of the schema-validity gap. A data-driven table of the XSD 1.0 child content model rejects, at compile time, any component child the model does not admit, more than one annotation (or an annotation that is not first, except on schema/redefine), and an identity constraint missing its selector or field. The check walks only XSD-namespace elements and skips xs:appinfo/xs:documentation foreign content, so no valid schema is rejected; restriction/extension use the permissive union of their contexts (never over-rejecting). One table covers every component rather than a rule hand-coded per element. XSTS: invalid schemas accepted 1688 to 1313 (-375), no other bucket raised.

  • The id attribute on XSD components is now validated at compile time (#147). It is of type xs:ID, so each value must be a valid NCName and unique within the schema document; the values were never checked, so a malformed (id="", id="123") or duplicated id left the schema wrongly accepted. The schema-document tree is walked, skipping xs:appinfo/xs:documentation foreign content (whose id attributes are not xs:ID) and treating only the unprefixed id (not a foreign xml:id/pre:id) as the component identifier, so no valid schema is rejected. XSTS: invalid schemas accepted 1789 to 1688, no other bucket raised.

  • Value-bound facet definitions are now validated at compile time (#147), continuing the facet-validity work. Per XSD Part 2 Datatypes 4.3, each minInclusive/maxInclusive/minExclusive/maxExclusive and enumeration value must be a valid value of the base type; minInclusive excludes minExclusive and maxInclusive excludes maxExclusive; and the lower bound may not exceed the upper (nor equal it when either side is exclusive, an empty value space), compared in the base primitive's value space. XSTS: invalid schemas accepted 2082 to 1789, no other bucket raised.

  • xs:gMonth now accepts the XSD 1.0 lexical form --MM-- (the trailing -- is optional, the later --MM erratum form still accepted), with an optional timezone (#147). Only --MM was recognized, so valid gMonth values and the schemas whose facets used them were wrongly rejected. Surfaced by the value-bound facet check above (which validates a bound value against its base type). XSTS: valid instances rejected 233 to 214; it also removed 26 false schema rejections the bound-facet check would otherwise have caused.

  • Constraining-facet definitions are now validated at compile time (#147), the first cut at the schema-validity gap (invalid schemas the validator was silently accepting). Per XSD Part 2 Datatypes 4.3, each length-family facet value (length, minLength, maxLength, totalDigits, fractionDigits) must be a valid nonNegativeInteger (totalDigits a positiveInteger), length may not co-occur with minLength/maxLength, and the integer ranges must be ordered. Malformed values (length="", "-1", "a", "1e2") were dropped by numeric parsing and the schema accepted; the definition is now read raw and rejected at compile time, with well-formedness checked independently of machine-integer range so a huge but valid value is not misreported. The findings travel through a shared diagnostics collector on the parsing context and join the model-level consistency findings. XSTS: invalid schemas accepted 2461 to 2082, with no change to any other bucket.

  • A built-in datatype named by xsi:type (xsd:int, xsd:decimal, xsd:string) is now resolved, gated by substitution validity (#147). The override resolved only against schema-declared types, so a built-in xsi:type was reported "unknown" and the element wrongly rejected. The built-in is now resolved when it validly substitutes for the declared type per Schema-Validity Assessment: the ur-type (anyType/anySimpleType) admits any type, and a bare built-in declared type admits any built-in derived from it through the XSD Part 2 datatype lattice (new BuiltinType.derives(from:)). An invalid built-in substitution is a located error, not a silent acceptance, closing the gap the resolution exposed. XSTS: valid instances rejected 248 to 233 (with no regression in invalid instances accepted, which the two fixes below preserve).

  • A simple-typed element now rejects non-schema-instance attributes (#147). A simple type declares no attribute uses, so an element whose type is simple, including one whose simple type comes from an xsi:type override and including an xsi:nil element, may carry only the xsi:* and namespace attributes; any other attribute is a violation. The check was absent, so resolving a built-in xsi:type to a simple type would otherwise have accepted stray attributes on it.

  • Identity-constraint fields compare in their value space when the selected node carries an xsi:type (#147). unique/key/keyref tuples compared lexically, so two xsi:type="xsd:decimal" fields 3.0 and 3, the same value, were treated as distinct and a unique violation was missed. Each field value now carries the simple type read from the node's xsi:type; two values are equal in that type's value space when both share a built-in base (reusing SimpleType.valueMatches), otherwise by lexical form.

  • Simple-content values preserve significant whitespace per the type's whiteSpace facet (#147). textContent trimmed leading/trailing whitespace unconditionally before the facet ran, so an xs:string (whiteSpace="preserve") value that was whitespace-only or had significant edges (e.g. matching a \t pattern, or counted by a length facet) was wrongly emptied or shortened. Simple-content validation now reads the raw character content and lets the facet normalize it; the element-only/empty "no significant text" check still ignores indentation whitespace. XSTS: valid instances rejected 288 to 248.

Added

  • Schema.Document.validate(_:schemaLoader:) honors an instance's xsi:schemaLocation and xsi:noNamespaceSchemaLocation (#147). Each referenced schema document is loaded through the supplied loader, compiled, and its global declarations merged in (the primary schema's own declarations win on conflict), so a strict or lax wildcard can resolve an element declared in another document. The plain validate(_:) is unchanged. XSTS: valid instances rejected 308 to 288 (the particle clusters whose wildcard-matched elements live in a separate, instance-referenced schema).

Fixed

  • A mixed="true" complex type with no content model now permits character data (#147). Such a type (only attributes, no sequence/choice/all) was compiled to the empty content type, which forbids text, so a mixed element carrying only text was rejected. It is now mixed content over an empty particle: text is allowed, child elements are still not. XSTS: valid instances rejected 311 to 308.
  • <xs:attribute ref="..."> references to global attribute declarations are now resolved (#147). A reference has no name, so it was dropped and the referenced attribute looked undeclared on the instance; nested attribute-group references were already flattened, but the per-attribute reference was not. The reference now resolves to the global declaration's type, taking use/default/fixed from the reference site. XSTS: valid instances rejected 338 to 311.
  • Document-scoped xs:ID/xs:IDREF validation is now enforced (#147). xs:ID, xs:IDREF, and xs:IDREFS were checked only lexically (as NCNames); the document-wide constraints, that every ID is unique and every IDREF/IDREFS item matches some ID, were not. The tree validator now records ID and reference values during its typed walk (elements and attributes) and reports duplicate IDs and unresolved references once the whole document is seen, so a forward reference resolves against a later ID. XSTS: invalid instances accepted 179 to 166.
  • The regex \w and \W escapes now follow the XSD definition (#147). \w was the Perl class (alphabetic, decimal, underscore); XSD defines it as [#x0000-#x10FFFF]-[\p{P}\p{Z}\p{C}], so symbols and marks are word characters and the connector _ (Pc) is not. \W correspondingly matched symbols it should not. XSTS: invalid instances accepted 231 to 210. (Internal regex fixtures that assumed _ was a word character were corrected.)
  • block="substitution" on a substitution-group head is now enforced (#147). The substitution filter only dropped members whose type derived from the head's type by a blocked method (extension/restriction); the substitution keyword, which forbids substitution-group substitution outright regardless of the member's type, was ignored, so a same-type member still substituted under a substitution block. XSTS: invalid instances accepted 238 to 231, valid schemas rejected 75 to 74.
  • Element default and fixed values are honored on empty elements (#147). An empty element with a value constraint takes that value, so the empty content is valid even when the type would reject the empty string; the constraint value (not the empty string) is what is validated against the type, including any xsi:type override. A restriction's enumeration now also replaces the inherited enumeration rather than unioning with it (the new set is the value space; pattern still accumulates and is ANDed), which a value-constraint case under xsi:type exposed. XSTS: valid instances rejected 395 to 369, invalid instances accepted 240 to 238.
  • Element-level block is now enforced against xsi:type substitution (#147). The block on an element declaration (distinct from block on its type) was parsed but never threaded to the instance validator, so a document substituting a blocked-derivation type via xsi:type was wrongly accepted. The element block is now carried through to blockedSubstitutionError, which also resolves the element-ref type indirection (element:foo to its concrete type) so the check fires for referenced children, not only the root. XSTS: invalid instances accepted 261 to 240.
  • The regex engine now recognizes the full XSD \p{Is<Block>} Unicode block set (#147). Only about fifteen blocks were tabulated, so a pattern over any other block (\p{IsArmenian}, \p{IsTamil}, \p{IsHangulJamo}, and so on) failed to compile and rejected valid instances. The table now carries the Unicode 3.1 blocks the XSD regular-expression appendix fixes (surrogate blocks omitted, since surrogates are not scalar values); each range is verified bidirectionally by the XSTS corpus (valid in-block instances pass, invalid out-of-block instances still fail). XSTS: valid instances rejected 502 to 395.
  • Length facets on xs:QName no longer reject valid values (#147). XSD 1.0 Datatypes 4.3.1 does not define a unit of length for QName (only string, anyURI, binary, and list), so length/minLength/maxLength on QName are implementation-defined; like Xerces and the XSTS NIST set we treat them as non-constraining rather than counting characters. Net XSTS: valid instances rejected 547 to 502 (36+ NIST false rejections fixed); 4 Microsoft cases that read QName length as a character count remain in invalid instances accepted (257 to 261) and are documented as a deliberate spec-ambiguity interpretation in docs/xsts-deviations.md.
  • Ordering facets on xs:duration are now enforced (#147). minInclusive, maxInclusive, minExclusive, and maxExclusive on duration were ignored because duration has no total order in the generic comparator. A DurationValue now implements the XSD 1.0 Datatypes 3.2.6.2 partial order (compare by adding to the four reference dateTimes); a value incomparable to the bound (such as P1M against P30D) is outside the range and rejected. XSTS: invalid instances accepted 355 to 257.
  • Pattern and enumeration facets on a union type are now enforced (#147). A union admits a value valid for any member, but pattern and enumeration (the only constraining facets XSD allows on a union) had been ignored, so a member-valid value outside the enumeration or failing the pattern was wrongly accepted. Enumeration compares in the value space of the admitting member (so 01 equals an enumerated 1 for an integer member), matching the atomic path. XSTS #147: invalid instances accepted 555 to 355.
  • Length facets on the built-in list datatypes now count list items, not characters (#146). A restriction of NMTOKENS, IDREFS, or ENTITIES resolved its base through neither the BuiltinType table (which has only the singular NMTOKEN) nor the user-type table, so it collapsed to an atomic string and length/minLength/maxLength measured characters, rejecting valid instances. The restriction base now resolves through simpleTypeReference, preserving the list variety. Closes the XSTS NIST list-NMTOKENS/IDREFS/ENTITIES facet clusters: valid instances rejected 604 to 547, invalid instances accepted 582 to 555.
  • Three crash and hang classes uncovered by running the full XSTS archive to completion (#129). (1) Nested occurrence explosion: the content-model NFA capped each particle's maxOccurs unroll at 16384 but not the total state count, so nested high-maxOccurs particles multiplied (a sequence of 16384 holding a choice of 16384 is 2.7e8 states) and OOM-killed the process at 8 GB on the Microsoft Particles set; the builder now also caps total states (2^20), degrading further repetition to star. The regex {n,m} quantifier builder had the same unbounded unroll and the same cap is applied. (2) Quadratic content-model validation: sequenceStructureErrors called follow(after: prefix) once per child and follow re-walked the whole prefix from the start each time, so a content model over many children spun at 100% CPU; matching is now incremental (one active state-set advanced per child), turning O(children squared) into O(children). (3) Character-class subtraction trap: the regex pattern [abcd-[d]] (XSD class subtraction) was parsed as a d to [ range, and since d (100) exceeds [ (91) the reversed ClosedRange trapped (SIGBUS); -[ now always opens a subtraction, and any reversed range (such as [z-a]) is a clean rejection rather than a trap.

Changed

  • Scanner-side validity findings are now located structured values (validation-framework audit follow-up): DocumentType.validityFindings carries ValidityFinding values (reason plus the declared name they are about) instead of strings, DTDSchema.declarationErrors holds ready ValidationErrors, and every declaration-level finding renders at its declaration's coding path (x, r/@a) instead of "at root of document", matching the XSD consistency-errors precedent. Finding texts are unchanged.

Added

  • W3C XSTS runner, first measurement (#129, #122): the W3C XML Schema Test Suite (2006-11-06 release, ~13k test groups across NIST, Sun, Microsoft, and Boeing contributions; W3C Document License, never vendored) runs opt-in via XSTS_ROOT: every schemaTest must compile when expected valid and be rejected when expected invalid, and every instanceTest validates against its group's schema with the expected outcome; hrefs resolve against the referencing file and include/import locations from disk relative to the schema. The four disagreement counts are asserted exactly; the first measurement landed 2026-06-13 and is baselined at 75 valid schemas rejected, 2467 invalid schemas accepted, 604 valid instances rejected, and 582 invalid instances accepted across 14383 groups, with per-case deviations written to /tmp/xsts-failures.txt for the burn-down. Building the runner immediately caught a denial-of-service bug: a schema with maxOccurs in the trillions (groupF009v) OOM-killed the process because the content-model automaton unrolled occurrence counts literally; unrolling is now capped at 16384 with larger repetitions treated as unbounded, the libxml2 posture. The roadmap mermaid records the closed behavioral-parity set (#141-#144, collation) and the #139 performance epic.

Added

  • The last parity items, to one hundred percent (#143, #144, #130): XPath gains a configurable protective evaluation budget (PureXML.XPath.Budget, with a libxml2Compatible preset matching its fixed 10M node-set cap), threaded through query evaluation and thrown as a located QueryError.budgetExceeded instead of libxml2's silent refusal, default unbounded; xsl:sort honors lang (an attribute value template) with tailored alphabet orders for Polish and Russian derived from the conformance golds (diacritics interleaved, Cyrillic Yo after Ye), case-folded, falling back to the default order for untailored languages; and the WASM gate executes the full test suite under wasmtime on an actual WASI runtime (build-only with a notice when wasmtime is absent), wired into CI. The first WASI run immediately caught a real wasm32 trap: format-number's digit extraction converted through Int, which is 32 bits on wasm32 and trapped at 2^31 despite the 2^53 guard; the internals are now explicitly Int64 and the xsl:number bound is platform-correct (Double(Int.max)). Xalan baseline 127 to 125.

Added

  • Public streaming surface (#142) and a public-API test target: EventReader.next(), both initializers, and the Event alias are public, so external consumers can pull events with bounded memory over arbitrarily large inputs (an internal typealias in a split file had been shadowing the public Parsing.Event, narrowing next() to internal; the benchmark harness found it). A new PureXMLPublicAPITests target compiles against the public surface without @testable, so any public facade over internal members fails CI; it pins parse/query/serialize, streaming pull, and the chunked pull source.

Fixed

  • XPath document-order sorting is benchmarked at scale across generated (4/42/210 MB) and real corpora (NASA ADC 25 MB, SwissProt 115 MB), all linear: the quadratic fix's own first attempt (an eager sibling table built per predicate evaluation) was caught by sampling the live run and replaced with a lazy table (single-node sets never touch order keys; a parent's table builds on its second lookup). XPath at 42 MB drops 27.1 s to 1.57 s (ratio 190x to 11.7x); at 210 MB PureXML completes in 9.4 s the query libxml2 refuses at its 10M node-set cap (the configurable protective budget is #143). The benchmark harness gains a size series, a CORPUS mode for real-world files, a SAX stage, and a refusal-aware comparison table.
  • XPath document-order sorting is no longer quadratic on flat fan-outs (#139, found by the large-corpus benchmark): document-order keys looked up each node's index by scanning its parent's children, so sorting a node-set drawn from one wide element (the 42 MB corpus: 200k siblings) cost 27 seconds. A per-operation sibling-index cache enumerates each distinct parent once, making key computation linear in the tree.

Changed

  • Byte-backed Reader, performance passes 2 and 3 (#139): String input now copies its UTF-8 once into owned storage (an unsafe pointer encapsulated in a class lifetime, never escaping the parser) and scalars decode straight off the pointer, eliminating the per-character closure chain; ASCII literals match and consume at the byte level; and plain-ASCII character data, the bulk of any document, scans as a single byte run with one String built per run. Cumulative parse time on the 4.2 MB benchmark corpus drops 32 percent (0.49 s to 0.34 s, libxml2 ratio 30x to 20x). The W3C conformance suite caught one boundary in the run scanner (a run starting with '>' could swallow the "]]>" check after the character path consumed "]]"; ibm14n01); the run scanner now leaves a leading '>' to the character path, with the case pinned as a unit regression. All suites (W3C xmlconf, RELAX NG, xalan), Linux, and WASM verified.
  • First measured performance pass (#139): Reader.matches() no longer materializes an Array of the literal per call (it runs on every scan decision and was the hottest comparison in the parse profile); parse time on the 4.2 MB benchmark corpus drops 16 percent (0.49 s to 0.41 s, libxml2 ratio 30x to 26x). The benchmark harness, baseline, profile findings, and the optimization log live in docs/benchmarks.md.

Fixed

  • Stylesheet composition cycles no longer crash (#130 critic loop): an include or import cycle (a.xsl includes b.xsl includes a.xsl) recursed without bound and killed the host with a stack overflow, a denial of service reachable from stylesheet data. The compile chain now tracks the active load chain by resolved href and drops re-entry, while diamond composition (two units legitimately loading the same sheet) keeps loading both copies with their own import precedences, per 2.6.2. id() also gains a per-document index built on first use instead of re-walking the whole document per call, matching key(). Cycle and diamond regression tests added.

Added

  • Top-level stylesheet parameters (#141), nineteenth xalan burn-down (#130): PureXML.XSLT.transform accepts parameters: [String: String], overriding xsl:param globals by name (xsl:variable globals are never overridable; the stylesheet records which names are parameters across imports and includes). The conformance runner honors the xalan harness contract, reading <case>.param files; en alphabet sorting passes, and the CRLF param files exposed a Swift grapheme gotcha (CRLF is one Character, so splitting on "\n" never splits; the runner splits on newline graphemes). Baseline 128 to 127; Polish/Russian alphabet tailoring stays baselined as the locale class.
  • Space-ignorable sort collation, eighteenth xalan burn-down (#130): xsl:sort's default text order now treats spaces as ignorable at the primary level, breaking ties by later-first-space (the Java Collator semantics the conformance golds encode), derived from the gold orderings rather than delegated to a locale library. Baseline 129 to 128.
  • DTD-typed id() and node-kind generate-id(), seventeenth xalan burn-down (#130): the XSLT layer's id() now follows the XPath definition exactly, resolving only attributes the source document's DTD declares as type ID (internal or external subset, parsed through the transform's entity resolver) and yielding the empty set without a DTD, replacing the name-based heuristic for transforms; and generate-id() produces non-empty, distinct identifiers for attribute and namespace nodes through their identity hash instead of returning the empty string. Two unit tests pin both. Baseline 131 to 129.
  • HTML 4 serialization corners, sixteenth xalan burn-down (#130): basefont and isindex join the void-element set (no end tags), and the html method leaves an ampersand unescaped when a brace follows, the historical &{...} attribute convention of 16.2. Baseline 134 to 131 (output 20 to 17).
  • Globals, prefix wildcards, fifteenth xalan burn-down (#130): a top-level variable now evaluates with every previously evaluated global in scope, so a variable whose body references an earlier top-level param (directly or via call-template) resolves; same-name globals resolve by import precedence (folds happen in precedence order and the last same-name entry wins, so a later sibling import beats an earlier one and the importing sheet beats both); the stylesheet fold uniformly favors later contributions for attribute-set definitions, decimal formats, namespace aliases, and output settings, matching the post-order precedence model; and the XPath compiler accepts the NCName:* name-test form, matching any name in the prefix's namespace (bound by URI, falling back to the in-document prefix string) at its -0.25 default priority, so @ped:* outranks @*. Three unit tests pin the behaviors. Baseline 144 to 134 (variable 10 to 5, conflictres 8 to 5).
  • Import precedence and relative composition, fourteenth xalan burn-down (#130): import precedences are assigned in post-order over the import tree, so sibling imports are distinct (later higher) and every unit sits above its own imports; apply-imports searches only the current template's stylesheet's own import subtree per 5.6 (templates carry their unit's precedence range), falling back to the built-in rule when that unit imports nothing; an included stylesheet's declarations join the including unit at its precedence while its imports join the import list; and include/import hrefs resolve against the including stylesheet's own URI, so nested relative chains load correctly. Also fixed along the way: an included document's tree was only weakly referenced once flattened, so its ancestor chain (xmlns declarations, exclusions in scope) could disappear before its templates were parsed. Two unit tests pin apply-imports scoping and relative chains. Baseline 159 to 144 (reluri 11 to 3, impincl 9 to 2).
  • Attribute sets, copied namespaces, html booleans, loader entities, thirteenth xalan burn-down (#130): same-name xsl:attribute-set declarations merge as ordered definitions per 7.1.4 (each expands its used sets before its own attributes, a later definition's attributes winning); xsl:copy of an element carries the source element's in-scope namespace nodes per 7.5 (the fixup pass drops result-redundant ones); the namespace fixup lets a carried attribute prefix shadow an inherited binding locally (matching the source document's shape) unless the element's own name relies on it; the html method minimizes boolean attributes whose value repeats their name (CHECKED="CHECKED" serializes as CHECKED); and external DTDs and entities referenced by the stylesheet or source now resolve through the transform's document loader, the same channel as document(). Four unit tests pin the new behaviors. Baseline 174 to 159 (attribset 13 to 4, copy 12 to 7).

Fixed

  • format-number and xsl:number no longer trap on extreme values (#130 critic loop): format-number of a value at or beyond 2^53 rendered through an unguarded Double-to-Int conversion, a fatal trap reachable from user data (format-number(99999999999999999999 * 99999999999999999999, '#,##0') crashed the host); a fraction picture with more than 15 places overflowed the scale conversion the same way; and xsl:number value= beyond 2^53 trapped identically. Digit extraction now clamps fraction places to 15 (the 2^53 exactness bound), pads back to the picture's requested minimum, renders out-of-range integers through the exponent expander (digits stay exact), and xsl:number falls back to plain XPath number rendering outside [1, 2^53). Regression tests cover all three paths.

Added

  • Critic-loop hardening of the xalan burn-down (#130): raw-output marker handling is now gated on the stylesheet actually mentioning disable-output-escaping, so a source document that happens to contain the private-use marker characters U+E000/U+E001 passes through every transform untouched (previously they were stripped, and text between them unescaped, unconditionally), and the text output method only removes markers without "unescaping" content that was never escaped; the pattern-matching function table is built once per transform instead of once per (template, node) membership test; and the d-o-e, CDATA "]]>" splitting, simplified-syntax, html default-method/META, per-document key(), and attribute-axes behaviors gain focused unit tests that run in CI without the opt-in corpus (eight new tests).

  • Following/preceding from attribute nodes, twelfth xalan burn-down (#130): the following and preceding axes now work from attribute and namespace starts, anchored at the owner element per the XPath 1.0 document order (an attribute follows its element and precedes its children, and has no descendants of its own). Baseline 182 to 174 (axes 16 to 8).

  • Keys per document, in document order, eleventh xalan burn-down (#130): key() resolves its index against the current node's own document (built lazily per document root, so keys work inside document() trees), a node-set use expression indexes the matched node under every member's string value, and key() results return in document order regardless of the lookup value order. Baseline 187 to 182.

  • Sort keys in context and cross-document template matching, tenth xalan burn-down (#130): xsl:sort keys are computed once per node with the node's own evaluation context (its position and size in the selection, the caller's variables, functions, and namespace bindings), so position()-based and variable-referencing keys work, and NaN keys order before every number under data-type="number" (the Xalan order); match patterns evaluate over the matched node's own document rather than the transform source, so templates match nodes loaded through document() (the match cache keys include the document identity). Baseline 204 to 187 (sort 18 to 11, mdocs 14 to 6).

  • Prolog and CDATA conformance, ninth xalan burn-down (#130): the XML declaration precedes the document type declaration; a public-only doctype is emitted by the html method (16.2) and ignored by the xml method, which requires doctype-system (16.1); the simplified stylesheet syntax of 2.3 (a literal result element carrying xsl:version) compiles as a single match="/" template; and cdata-section-elements text containing "]]>" splits across section boundaries so no section holds the terminator. Baseline 218 to 204 (output 34 to 20).

  • Stylesheet namespace resolution, eighth xalan burn-down (#130): a template carries the xmlns bindings in scope at its stylesheet element, and they flow into its match pattern (the match cache keys on pattern plus bindings) and every expression evaluated while it is instantiated, so match="baz2:doc" and select="local-name(baz2:b)" resolve the prefix against the stylesheet and match by namespace URI; with bindings supplied the XPath 1.0 rule applies exactly, an unprefixed name test selecting only the null namespace. The instruction-scope namespace capture includes the default declaration, so xsl:element honors an in-scope xmlns="uri" for its unprefixed created names, an in-scope xmlns="" keeps them namespace-free (undeclared in the result when an inherited default would capture them), and an unusable created name (:foo) drops the wrapper and keeps the content, the Xalan recovery. Baseline 263 to 218 (namespace 41 to 13, axes 16, sort 18, output unchanged at 34, the rest spread).

  • Keys, ids, and doctyped sources, seventh xalan burn-down (#130): key() and document() read the string value of attribute and namespace node arguments instead of dropping them (the Muenchian grouping idiom works); match patterns evaluate with the XSLT function table, so key('k', 'v') and id('x') patterns match (a key/id pattern branch is also no longer prefixed with //); and the XSLT entry points parse sources, stylesheets, and document() loads with doctypes allowed, which the conformance corpora use throughout. Baseline 292 to 263 (idkey 39 to 16, the rest spread).

  • Templates over attribute and namespace nodes, sixth xalan burn-down (#130): xsl:apply-templates and xsl:for-each now keep attribute and namespace nodes in their selections instead of dropping them at the tree-node conversion. The evaluation context carries the non-tree current node (its owner element backs the tree-only machinery), XPath evaluation and current() start from it, match patterns select attribute nodes (the match cache keeps attribute identities, owner plus name, alongside tree identities), the built-in rules copy an attribute's or namespace's value as text, xsl:copy copies the focused attribute or namespace declaration itself, and an attribute result arriving after element content is ignored (the XSLT recovery). Sorting and apply-imports work over the generalized node kind. Baseline 352 to 292 (axes 35 to 16, copy 29 to 16, attribset 18 to 13, the rest spread).

  • Output methods in depth, fifth xalan burn-down (#130): disable-output-escaping on xsl:value-of and xsl:text (raw text travels bracketed by private-use sentinels and the serializer's escaping is undone after the fact; a string extracted from a result tree fragment loses the markers per 16.4, so the variable round-trip case re-escapes); the default output method is html when the first result element is named html with a null namespace (16.1); the xml method writes the XML declaration unless omit-xml-declaration="yes" (the spec default, with the caller's emit options as fallback only on explicit stylesheet silence); the html method injects META http-equiv="Content-Type" into head (16.2), keeps SCRIPT/STYLE raw and void elements end-tag-free case-insensitively, and escapes U+00A0-U+00FF to the HTML 4.01 Latin-1 entity names. Whitespace-only detection in stylesheet/source stripping now means XML whitespace only, so an NBSP-only text node survives. The conf comparison treats whitespace adjacent to tag boundaries as layout for non-canonical comparisons. Baseline 371 to 352 (output 49 to 30).

  • Literal result element namespace copying, fourth xalan burn-down (#130): a literal result element now copies its in-scope namespace declarations to the result per 7.1.1, minus the XSLT namespace and the namespaces of the prefixes listed in exclude-result-prefixes/extension-element-prefixes in scope (#default included); an aliased stylesheet namespace declares its result prefix and URI instead. The namespace-fixup pass drops any declaration that repeats an inherited binding, so the copied nodes do not redeclare on every descendant, and created names now reuse the stylesheet's own prefixes (anamespace:Attr1 rather than ns0:Attr1). xsl:output indent="yes" switches to the corpus golds' Xalan indentation form: children on their own lines with no leading spaces. Two unit expectations updated to the spec behavior (a literal element under stylesheet-level declarations carries them; collapsed alias+result declarations). The xalan baseline drops from 446 to 371 (namespace 89 to 42, output 50 to 49 and others spread across categories).

  • format-number picture parsing in full, third xalan burn-down (#130): the picture now parses JDK DecimalFormat-style into literal prefix/suffix affixes around the number part, the negative subpattern after the pattern separator (its affixes apply; without distinguishing ones the minus sign still prefixes, the Xalan resolution, which also covers colliding decimal/grouping separators by reading every occurrence as grouping), per-mille (with a custom symbol via the new per-mille attribute) and percent as multipliers kept literally in the output, the grouping size taken from the last grouping separator's distance to the decimal point instead of a fixed 3, and the generic currency sign U+00A4 rendering as this processor's en-US $. Digit extraction is rewritten over one scaled rounding plus pure integer arithmetic, so 0.4812 renders 4812 rather than re-accumulating float error per digit. The xalan numberformat category drops 30 to 1 (the survivor is a namespace-copy case); baseline 475 to 446.

  • Full xsl:number per XSLT 1.0 section 7.7, second xalan burn-down (#130): level single/multiple/any with count and from as real patterns matched through the transform's match cache (default count: same node type and expanded name; single finds the nearest matching ancestor-or-self and numbers it among matching preceding siblings, multiple numbers every matching ancestor-or-self outermost first, any counts matching nodes at or before this one in document order with from as the boundary, always yielding a number so an unmatched count renders 0); the value expression evaluates and rounds, bypassing formatting below one; and the full format-token engine (leading/trailing punctuation, alternating alphanumeric tokens and separators with last-token and last-separator reuse, zero-padded decimal widths, a/A/i/I and the Greek alphabetic sequence, grouping-separator/grouping-size digit grouping). A single/multiple level whose count matches nothing renders as the empty string, punctuation included. The xalan baseline drops from 557 to 475 (numbering 84 to 7, the rest pattern-engine prefixed/key/id cases).

  • document() is stable per transform (#130): the same URI reference now returns the identical document object across calls within one transform, per the XSLT document() definition, via a per-transformer document cache; previously each call re-parsed, so node identity, and with it generate-id() uniqueness against live nodes, could be violated when an address was reused (the idkey49 flake).

  • XSLT namespaced name creation and result fixup, first xalan burn-down (#130): xsl:attribute and xsl:element honor the namespace attribute and resolve a prefixed name against the instruction's own in-scope stylesheet declarations (7.1.2/7.1.3, captured at parse time so AVT names resolve correctly); xsl:copy applies use-attribute-sets; and a namespace-fixup pass runs over the result tree before serialization, declaring every namespace a created element or attribute name carries, reusing an existing binding for the same URI or generating ns0, ns1, ... prefixes when the carried prefix is absent or taken, and undeclaring an inherited default namespace that would capture an unqualified name. The xalan baseline drops from 604 to 557 (attribset 52 to 20, namespace 123 to 92, axes 68 to 35); one namespace-alias unit test updated to expect the now-declared result namespace.

  • XSLT 1.0 conformance runner over the Apache xalan-test corpus (#122, #130): the de facto OASIS-era suite (1690 gold-bearing cases across 36 categories) runs opt-in via XALAN_TS_ROOT, never vendored. Comparison is deliberately normalized: outputs that parse as XML compare by canonical form (Xalan's indentation is not normative), others with whitespace runs collapsed. First measurement: 1086 pass; the 604 known failures are carried exactly in Tests/Fixtures/xalan-baseline.txt, asserted in both directions (a fixed case must leave the file, a regression shows as a new failure). The top burn-down classes by count: namespace handling (123, led by namespaced xsl:attribute creation), xsl:number depth (85), axes corners (68), output/serialization options (55), attribute sets (52), id()/key() (41).

  • RELAX NG schema-correctness validation (#131): a schema document is now validated before any pattern interpretation, in three layers. The grammar check (section 3) rejects unknown or misplaced elements, illegal unqualified and RELAX NG-namespace attributes, wrong child arity, stray text, malformed or undeclared-prefix QNames, non-URI datatypeLibrary values (absolute, fragment-free, valid percent escapes), fragment-carrying hrefs, the 4.16 except restrictions (anyName/nsName inside excepts, propagated through choice), and the xmlns name prohibitions including the ns-attribute and except-mention forms. The compiler checks reject combine conflicts and double combine-less defines (4.17), unresolved refs and parentRef without a parent, startless grammars, include/externalRef self-reference, include overrides with nothing to override, non-grammar include targets (loaded documents are grammar-checked too), unknown datatypes, and params against the built-in library. The restrictions checker (4.19, 7.1-7.4) runs over the compiled algebra after 4.20/4.21 normalization (notAllowed/empty operands simplify away first; recursion detection deliberately precedes it): prohibited paths under attribute, oneOrMore-group, list, and in start; computable content types; duplicate attributes by representative-name overlap; infinite-name-class attributes outside oneOrMore; and the interleave text/element constraints. The spec suite's incorrect class drops from 213 compiling to 7, all pre-Fifth-Edition name-class cases (the xmltest-141 class, count asserted), with zero correct-schema or instance regressions. Two leniency tests updated to the spec behavior (double combine-less defines and unknown datatypes are schema errors).

  • RELAX NG QName values, instance level clean (#131): <value type="QName"> compiles to its (namespace, local-name) value pair via the schema's xmlns/ns scope, and the derivative engine threads the instance element's in-scope namespace bindings down to value comparison (in both the tree walk and the streaming state, whose frames now carry their bindings), so an instance QName resolves its prefix, or the default namespace when unprefixed, against the instance document itself; an unbound prefix matches nothing. The streaming path also gains the tree walk's empty-element rule. With this the spec suite's instance level is fully clean: valid 289/289 accepted and invalid 291/291 rejected; only the asserted schema-correctness class remains.

  • RELAX NG grammar scoping and include depth, third burn-down (#131): each grammar element opens its own define scope (names are scope-mangled in the flat table, so nested grammars do not collide) and parentRef reaches the enclosing scope; include is transitive (an included grammar's own includes merge first) and a define carried by the include element replaces the included grammar's same-name define (the 4.7 override); div wrappers flatten into their grammar while still contributing ns/datatypeLibrary inheritance (4.11); value text is kept untrimmed so whitespace-preserving datatypes compare exactly (6.2.9); and dataExcept recurses through choice patterns. The spec suite is now clean except one class: valid 286/289 and invalid 287/291, with all six remaining instance cases the QName datatype (its value space needs prefix resolution in both the schema and instance contexts), plus the asserted schema-correctness item.

  • RELAX NG namespaces and semantics, second burn-down (#131): the compiler now implements ns inheritance and the QName form of names (4.9-4.10: ns walks ancestors and crosses include/externalRef document boundaries; name="eg:foo" and <name>eg:foo</name> resolve their prefix against the schema's in-scope xmlns declarations; the name= shorthand on an attribute stays namespace-less rather than inheriting), an include brings in the included grammar's start with an include-level override honored (4.7), multiple <start> elements combine like defines and a single define or start may omit combine while still merging by the named method (4.17), <data> supports <except> via a new dataExcept pattern in the derivative engine (4.12), an empty element matches a data/value pattern whose lexical space admits the empty string (Clark's empty-text rule), and a whitespace-only attribute value matches a pattern that matches empty. Spec-suite baselines tighten from valid 242/289 to 274/289 and invalid 278/291 to 279/291; the name-class and tree-helper sections split to their own files for the length caps.

  • RELAX NG simplification, first burn-down (#131): foreign-namespace elements are stripped before pattern interpretation (4.1: only RELAX NG-namespace elements are schema content; a namespace-less element counts only inside a fully unqualified schema), name/type/combine attribute values are whitespace-trimmed (4.2), and include/externalRef hrefs resolve against the xml:base attributes in scope and the loaded document's own URI (4.5, reusing the RFC 3986 resolver), with the document base threaded through nested loads and visited-tracking keyed on resolved hrefs. Spec-suite baselines tighten: valid 242/289 accepted (from 233), invalid 278/291 rejected.

  • RELAX NG spec test suite runner (#122, #131): James Clark's spectest.xml (385 cases, distributed with jing-trang) runs via an opt-in RNG_TS_ROOT runner, never vendored; the manifest is parsed with PureXML itself, nested testSuite scopes thread their resource/dir definitions into the compiler's loader, and each case asserts compile/reject for the schema plus accept/reject per instance. First measurement, carried as exact baselines: every correct schema compiles and most instances behave (valid 233/289 accepted, invalid 277/291 rejected), while schema-correctness validation is absent (all 213 incorrect schemas compile, count asserted exactly). The burn-down classes are recorded in the runner: schema validation against the RELAX NG grammar and the 4.16-4.18 restrictions, foreign-namespace stripping (4.1), xml:base-aware include/externalRef resolution (4.5), and the section 6 compatibility datatypes.

  • Canonical XML 1.0 spec-example vectors (#122, #132): the worked examples of the C14N 1.0 Recommendation's section 3 run as input/expected-output tests (cited per example; used under the W3C Software and Document License), all six passing: top-level PIs/comments with and without comments mode, whitespace preservation, start/end tags with attribute sorting and DTD defaults, character modifications and references, entity expansion, and UTF-8 output from legacy-encoded input. The vectors exposed three real conformance fixes: top-level siblings in the canonical form are now separated by single line feeds; an empty default-namespace declaration renders only when it undeclares a rendered non-empty default (superfluous xmlns="" is dropped, inclusive mode); and the text/attribute escape loops iterate Unicode scalars, so a carriage return that grapheme-clusters with a following line feed still escapes to &#xD; (the #135 class, surfacing in output rather than lexing).

  • Scalar-level lexing (#135): the reader now buffers exactly one Unicode scalar per element (multi-scalar graphemes from any source are split, string input iterates scalars directly), so a combining mark directly after an ASCII delimiter can no longer merge with it into one Swift grapheme cluster and XML's scalar-defined name productions classify correctly: U+06D6 lexes as a Fifth Edition PI-target NameStartChar, ZWNJ/ZWJ attribute names parse, and U+309A markup from entity expansion forms a real element. The replacement-text, content-model, and ATTLIST sub-scanners and the entity-literal validator go scalar-level the same way, while grapheme clusters reassemble naturally wherever scanned text is appended to a String (asserted by test). This clears the last eduni baseline (now empty), grows the IBM pre-5e class from 279 to 300 exactly as the Fifth Edition prescribes, and, combined with the EDITION="1 2 3 4" exclusions the xmltest manifest itself carries for cases 140/141, empties the xmltest deviation baseline entirely: every xmlconf case applicable to a Fifth Edition, namespace-aware processor now passes.

  • Strict internal-subset profile (#128): Limits(strictInternalSubset: true) holds the internal DTD subset to the letter of XML 1.0, rejecting conditional sections and parameter-entity references inside markup declarations (entity values, content models, ATTLIST bodies), both of which PureXML otherwise supports as features; DeclSep references between declarations and %name; inside quoted attribute defaults (AttValue, where % is literal) stay legal, and defaults are unchanged. The xmltest runner opts in, taking its baseline from 181/186 to 185/186 with one remaining documented deviation: case 141, which expects pre-Fifth-Edition name-character classes.

  • Per-entity base-URI tracking (#138): a relative system identifier declared inside an external entity now resolves against that entity's own URI (RFC 3986, reusing the C14N resolver), so nested external entities find their siblings; ExternalID gains an optional base and a resolvedSystemID, the DTD scanner threads the current entity's URI through external-subset and parameter-entity scans, and resolvers receive the already-merged identifier (raw identifiers unchanged when no base applies). Clears eduni errata-2e/E18; the eduni baseline now holds only the #135 grapheme-lexing class.

  • Encoding-declaration contradictions are fatal (#137, 4.3.3): a document whose XML declaration names a 16- or 32-bit encoding over BOM-less 8-bit bytes is rejected; a BOM followed by a declaration outside its family (a UTF-8 BOM with encoding='iso-8859-1', a UTF-16 BOM with an 8-bit name) is rejected, with the declaration read through the BOM's own byte pattern; and an external entity whose text declaration names a higher XML version than the document's is refused (errata E38), in both the external-subset and external-entity paths. The eduni not-wf baseline is now completely empty.

  • Namespaces 1.0 constraints (#136): the namespace layer now enforces the binding rules as located parse errors: malformed qualified names (foo:, :foo, a:b:c, xmlns:) are rejected; the xmlns prefix may never be declared; xml may be bound only to its own namespace name, which no other prefix (nor the default) may take, likewise the xmlns namespace name; a prefix may not be undeclared (Namespaces 1.0); attributes must be distinct by expanded name (the same URI+local name through two prefixes is a duplicate); PI targets and entity/notation names are NCNames; and a namespace declaration whose attribute is DTD-declared with a tokenized type binds its normalized value (so urn:x and urn:x are one namespace name, eduni ns 012). Clears all 18 namespace cases from the eduni baseline, leaving only the four #137 encoding-mismatch cases; xmltest valid/sa/012 (an attribute literally named :) joins the documented deviations per the suite's own NAMESPACE='no' flag, mirrored by the eduni runner's new NAMESPACE filter.

  • W3C xmlconf eduni sections (#121, #127): the Edinburgh errata suites (2e, 3e, and the 5th-edition 4e), Namespaces 1.0 with its errata, and the misc cases run via the XMLCONF_ROOT runner; XML 1.1 and Namespaces 1.1 are explicitly out of scope and EDITION-gated cases skip when they do not apply to the Fifth Edition. The burn-down fixed a real spec bug and several errata behaviors: attribute values now get the 3.3.3 CDATA normalization (literal whitespace becomes a space before reference decoding, so character-referenced whitespace survives, fixing errata E36 and E20 semantics) and tokenized normalization collapses only SPACE characters; an undeclared general entity in content or an attribute value is a deferred validity finding instead of a fatal error when unread external declarations might supply it (production 68; the reference stays literal, nothing is fetched), matching the scanner-side split; the errata E15 family is enforced (any reference, comment, PI, or whitespace inside an element declared EMPTY; direct or double-escaped character-reference whitespace as character data in element content, while entity-supplied whitespace stays legal); errata E2 reports duplicate tokens in enumerated and NOTATION declarations; errata E14's declaration-completed-inside-a-PE-replacement parses and reports VC: Proper Declaration/PE Nesting; the ID family must be NCNames under namespace validation; and DTD-strict mode requires namespace declarations to be declared like any attribute. Remaining deviations are exact and classified: 24+1 grapheme-cluster lexing cases (a combining mark after an ASCII delimiter merges into one Swift Character), 18 namespace-constraint cases, 4 encoding-declaration mismatches, and E18 (per-entity base-URI tracking), each filed as its own issue.

  • W3C xmlconf japanese section, fully clean (#121, #126): all 13 cases (the same prose and parameter-entity DTDs encoded as UTF-8, UTF-16 in both byte orders, Shift_JIS, EUC-JP, and ISO-2022-JP) decode through PureXML's own byte decoder, parse, and DTD-validate cleanly with zero deviations; the optional encodings the suite classifies as TYPE='error' are all supported, so they are held to the must-parse bar.

  • W3C xmlconf IBM section, clean to two documented classes (#121, #125): the deepest block, 1094 cases. Valid 220/220 (parse + strict DTD validation), invalid 64/64 reporting, not-wf 529/810 rejected with the 281 accepted cases being exactly two documented classes: 279 cases from productions 85-89, the 1998 character-class appendices deleted by XML 1.0 Fifth Edition (whose count is asserted exactly so drift in either direction is caught), and 2 parameter-entity-in-internal-subset feature cases (the xmltest 160-162 class, headed for #128's strict profile). Closing the section's real gaps: a document without a root element is rejected (production 1); an entity reference in an entity literal must be a lexical Name or character reference (production 68); WFC: No Recursion is enforced at declaration over the combined general/parameter reference graph (CDATA-shielded), so a recursive pair is rejected without being referenced; an undeclared parameter-entity reference is a well-formedness error when the document is standalone or has neither an external subset nor parameter entities, and a deferred validity finding otherwise (the production-68 WFC/VC split, also applied to entity references in attribute defaults); a declared external PE the resolver refused is neither; and a content-model group opening in one PE replacement and closing in another is reported (VC: Proper Group/PE Nesting).

  • Standalone validity constraints (#134): a document declaring standalone='yes' is now checked against its external-subset dependencies (2.9). The DTD scanner records declaration provenance (which entities, element models, and attribute lists came from the internal subset); the validator reports an externally-declared attribute default that would be supplied, an attribute value an externally-declared non-CDATA type would normalize, and whitespace inside externally-declared element content. The parser enforces the matching well-formedness side: a standalone document referencing an entity declared outside the internal subset reports it as undeclared (WFC: Entity Declared). This empties the entire Sun baseline: the section is fully clean at not-wf 56/56, valid 28/28, invalid 74/74, zero deviations, closing #124.

  • DTD validity constraints, second Sun burn-down (#121, #124): the validator now enforces VC Root Element Type (the root element must match the DOCTYPE name, recorded by the scanner), VC Unique Element Type Declaration (duplicate <!ELEMENT> reported, first stays in effect), VC No Duplicate Types in mixed content, VC One ID per Element Type, VC Notation Attributes (listed names must be declared) and VC Notation Declared (an NDATA entity's notation must exist), VC Attribute Default Legal across the tokenized types plus VC ID Attribute Default (an ID attribute must be #IMPLIED or #REQUIRED), lexical Name checks for ID/IDREF/IDREFS values, and CDATA sections as character data in element content even when empty. Strict mode additionally requires every attribute to be declared (VC Attribute Value Type; namespace declarations exempt). Declaration-level findings are computed once when the schema is built and reported at the document root. Sun invalid coverage moves from 43/74 to 62/74 reporting; the twelve remaining silent cases are one family, the standalone='yes' VCs, which need declaration-provenance tracking.

  • Entity content splicing (#133): a general entity whose replacement text contains markup, directly or through entities it references, is now reparsed as content at the point of reference (4.4.2 Included), so <p>text</p> inside an entity becomes a p element in the tree and in the pull-event stream, in both strict and recovering modes, instead of being included as escaped character data. The replacement is validated (the reference-time balanced-content WFC), checked for reference cycles, and budgeted against the amplification cap before being spliced into the reader's stream. Character references in internal entity literals now expand when the declaration is parsed for general entities too (4.4.5, previously parameter entities only), so &#60;foo> stores real markup that splices, and the Appendix D double escape &#38;#38; stores &#38; and renders as a literal ampersand; the reference-time WFC accordingly validates remaining references instead of re-expanding them. Text-only entities keep the single-pass expansion path and its event coalescing. Closes the last two Sun valid deviations: the section is now 28/28 valid.

  • Validity-layer conformance, first Sun burn-down (#121, #124): tokenized attribute values are normalized per 3.3.3 before validity checks (whitespace runs collapse, outer whitespace strips, CDATA untouched), so nonce satisfies an enumeration and an entity-expanded IDREF resolves; an external parsed entity's leading text declaration is stripped per 4.3.1 (and validated: version optional, encoding required, standalone forbidden) instead of being included in the replacement text; character references in parameter-entity literals are expanded at declaration time per 4.4.5, making the spec's Appendix D &#37;zz; example parse; and a PI target inside the DTD subset must be separated from its data (production 16). Sun baselines tighten to not-wf 55/56, valid 26/28, invalid 43 reporting; the two remaining valid failures share one cause, filed separately: general-entity replacement text containing markup is included as character data rather than reparsed into elements.

  • W3C xmlconf Sun section runner (#121, #124): the first validity-layer runner. Sun's manifests classify cases three ways and the runner checks all three: not-wf rejected by the strict parser (54/56), valid documents parse AND DTD-validate cleanly (19/28), invalid documents parse but must report at least one validity error (43/74). The exact deviation baselines are carried in the test with their causes: tokenized attribute-value normalization (3.3.3), external-entity text declarations (4.3.1), declaration-time character-reference expansion in PE literals (4.4.5), the standalone VCs, ID/IDREF corners, #REQUIRED enforcement, per-type attribute VCs, and the root/DOCTYPE name match, the work list this issue burns down.

  • W3C xmlconf OASIS/NIST section, fully clean (#121, #123): a second opt-in runner (XMLCONF_ROOT-gated, suite never vendored) drives the oasis section from its own manifest, parsed with PureXML itself; all 247 not-wf cases are rejected and all 100 well-formed (valid + invalid) cases parse, with zero deviations (the one excluded case is flagged NAMESPACE='no' by the manifest itself: pre-namespace colons in names, which a namespace-aware parser correctly refuses). Closing the section's last 24 gaps made the DTD subset grammar strict: unknown, lowercase, or space-separated markup-declaration keywords are rejected; a parameter-entity reference must be '%' Name ';' exactly; entity, parameter-entity, and notation names must start with a NameStartChar; NDATA requires whitespace and a strict name; a notation declaration requires its identifier and a clean tail; entity value literals admit no bare '%' (production 9); conditional-section keywords are case-sensitive INCLUDE/IGNORE and sections must balance to their ']]>'; junk between declarations is not well-formed; a grammar violation in the external subset now rejects the document (previously swallowed); the external subset may open with a text declaration (version optional, encoding required, standalone forbidden, production 77); and a processing-instruction target must be separated from its data by whitespace (production 16). The new strictness also tightened the xmltest baseline from 180/186 to 181/186: a CDATA section in the internal subset (case 107) is now correctly rejected by the conditional-keyword check, leaving five documented deviations.

Changed

  • Schema-consistency checks now run through the validation framework (#117 follow-up): the derivation final check and Particle Valid (Restriction) are composable Validation<SchemaTypeFact, CompiledSchemaFacts> rules (finalRespected, restrictionsAreSubsets) applied per named type, so a schema with several problems reports them ALL at once. SchemaError.finalViolation and .invalidRestriction are replaced by .inconsistent([String]) carrying every finding with its type's coding path; the finding texts are unchanged. checkRedefine, checkAllGroups, and notASchema stay fail-fast throws because they examine the raw schema source, the compilation analog of well-formedness.
  • Document-order sorts no longer recompute root paths per comparison (#113): node-set ordering (the evaluator's order-and-dedup, EXSLT's set functions, string-value coercion's first-node lookup) goes through decorate-sort-undecorate helpers (sortedByDocumentOrder(), firstInDocumentOrder()) that compute each node's order key once, turning O(n log n x depth) sorts into O(n log n). set:leading/set:trailing also reuse one precomputed pivot key. Behavior is unchanged.
  • XPath compilation moved out of the per-node loops (#112): XSLT template selection now resolves each match pattern once per transform through a per-run match cache (the pattern's node set is computed once over the source tree, then membership is an identity lookup), instead of recompiling and re-walking the tree for every (node, template) pair; and the XSD identity validator compiles each selector and field XPath once per run through a query cache instead of once per visited element. Schematron already stored compiled queries. Behavior is unchanged; a multi-template shape test guards the path.
  • The HTML document builder's body is now constructed as a live mutable TreeNode tree with a single open-elements stack (the HTML5 model), attaching nodes to their parent as they open rather than accumulating children on close (#83, internal). Behavior is unchanged; this is the foundation the adoption agency algorithm needs to reparent already-built subtrees.
  • DTD attribute validation is now decomposed into five named, independently composable Validation rules (#101): DTD.requiredAttributes, fixedAttributeValues, enumeratedAttributeValues, tokenizedAttributeTypes, and notationAttributes, replacing the single attributeDeclarations rule. Each is removable by identity and isolation-tested one constraint at a time, honoring the validation-framework idiom. Behavior is unchanged. (XSD content validation stays one recursive rule by a documented scope decision, since its constraints are interdependent through type resolution; Schematron already exposes its rules as composable Validation<Node, Void> values.)

Added

  • Parser strictness, first W3C-suite pass (#105 Tier 2, #120): the strict parser now rejects -- inside comments (including the ---> ending), a raw < in attribute values, the literal ]]> sequence in character data, attributes not separated by whitespace, and characters outside the XML Char production in content, five new located ParseError cases. Driven by a new opt-in runner for the W3C XML Conformance Test Suite's xmltest section (XMLTS_ROOT-gated; the suite is never vendored because its license permits redistribution only as the unmodified archive): valid/sa 118/119, not-wf/sa rejection improved from 80 to 180 of 186 and valid/sa to 119/119 across five strictness passes (the second adds character-reference validation, lowercase-x-only hex references and the referenced code point must be a valid XML Char; the reserved 'xml' PI target, legal only as the declaration at the document's very first bytes; and XML Char validation in comments and PI data; the third validates content models against the XML 1.0 grammar at DTD scan time, rejecting unbalanced parentheses, #PCDATA misplacement and nesting, mixed connectors, whitespace-detached or doubled quantifiers, empty groups, and SGML leftovers; the fourth makes the DTD declaration grammar strict: whitespace and name rules, junk after a value, NDATA spacing and its general-entities-only rule, entity-value literals checked for XML Chars and complete references, SYSTEM/PUBLIC identifiers parsed strictly with PubidChar validation, validated against productions 52-60, the reserved xml PI target rejected inside the subset, strict UTF-8 decoding, the XML declaration's version/encoding value grammar, and required whitespace between pseudo-attributes; the fifth implements the replacement-text well-formedness constraint: when a general entity is referenced, its replacement (character references expanded as at declaration) must reparse as balanced content in isolation, so a bare or incomplete reference, a raw '<' in an embedded attribute value, tags spanning the entity boundary, and a reserved-target PI inside the replacement are rejected at the point of use, while an unreferenced entity may carry any value and only the first declaration of a name binds, both per spec; references in attribute-list defaults must name already-declared internal entities with a non-recursive chain; and a CDATA section inside replacement text now shields its content from reference expansion. The six remaining suite deviations are deliberate: internal-subset conditional/CDATA sections and parameter-entity references are supported as features, and one case expects pre-Fifth-Edition name-character classes), with the exact remaining baseline carried in the test and the two remaining leniency classes (internal-subset DTD grammar, entity replacement-text well-formedness) tracked as #120.
  • XSD Particle Valid (Restriction) (#117): a complex type derived by restriction is now checked at schema compile to accept a structural subset of its base, covering element name matching, element-vs-wildcard admission, wildcard narrowing, occurrence-range subsumption, and the group recursions (sequence/all order-preserving with emptiable skips, choice mapping, sequence-into-choice, sequence-into-all, RecurseAsIfGroup). A structurally unfaithful restriction (widened occurrence, new element name, reordered sequence, content added to an EMPTY or required-into-empty base, an element outside a base wildcard's namespace) throws SchemaError.invalidRestriction with the type, base, and reason. The spec's effective-total-range arithmetic for MapAndSum is approximated by per-particle checks, a documented simplification.
  • Conformance corpus harness (#105, Tier 2): the start of conformance-suite checking, expressed through the validation framework (the OpenAPIKit idiom). A PureXML.Validation.ConformanceCase is a Validatable named expectation (actual vs spec-authoritative expected output), and Conformance.matchesExpected is a Validation<ConformanceCase, Void> rule that emits one located ValidationError per diverging case, so a whole suite reports every failure at once with the case name as its coding path. Seeded with a Canonical XML 1.0 corpus (attribute ordering, empty-element expansion, comment removal, CDATA-to-escaped-text, processing instructions, namespace rendering) an XPath 1.0 core-function corpus, and an XSD datatype/facet corpus (length, range, pattern, enumeration, lexical space), a RELAX NG pattern corpus (text, optional, repetition, choice, ordered group, attribute, interleave, empty), an XSLT 1.0 transformation corpus (value-of, for-each, if, choose, sort, variable, format-number, apply-templates, call-template), an HTML5 tree-construction corpus (void elements, implied tag closing, case normalization, the adoption agency, entities), a Schematron corpus (assert, report, rule context), a C14N exclusive/2.0 corpus (unused-namespace dropping, render-at-use, whitespace trimming), an XPath axes/predicates corpus (descendant, ancestor, sibling axes, positional and attribute predicates, union), a DTD content-model corpus (sequence, choice, occurrence indicators, mixed, EMPTY, ANY), an XInclude/XPointer corpus (substitution, text inclusion with escaping, fallback, relative href resolution; shorthand ids, element() navigation, xpointer() including the per-step predicate semantic), an XSD regex corpus (implicit whole-string anchoring, the character-class escapes, Unicode categories, quantifier ranges, alternation, class negation and subtraction), and an XML Catalog corpus (system/public/uri entries, rewrite prefixes, unmatched identifiers). The HTML corpus surfaced its first divergence (the fragment parser does not reconstruct active formatting elements like the document parser, tracked as #109). Growing the corpora against the official W3C/OASIS suites is the ongoing Tier-2 work.
  • EXSLT extension functions (#105, Tier 3): the common, math, and sets EXSLT modules, dispatched by resolving a function's prefix to its namespace (so any prefix bound to an EXSLT namespace works). math:min, math:max, math:highest, math:lowest, math:abs, math:sqrt (with the EXSLT NaN rule for non-numeric nodes); set:distinct, set:difference, set:intersection, set:has-same-node, set:leading, set:trailing (by node identity and document order); exsl:object-type. math:power and the other transcendentals are omitted because they need a C math library, which the pure-Swift, Foundation-free target excludes; exsl:node-set awaits result-tree-fragment support.
  • Canonical XML 1.1 (#105, Tier 3): a Canonical.Options.canonical11 preset and a mergeInheritedBase option. When canonicalizing a document subset, the xml:base values of the apex's omitted ancestors are merged into the apex by RFC 3986 reference resolution (vs. 1.0's nearest-ancestor rule), the apex's own relative xml:base is resolved against that chain, and xml:id is no longer inherited; xml:lang and xml:space still inherit the nearest. A self-contained RFC 3986 resolver (scheme/authority/path parsing, path merging, dot-segment removal) backs the merge.
  • Single-byte legacy encodings (#97): the byte decoder gains a table-based single-byte path and the full set of single-byte legacy encodings, selected by the XML declaration's encoding name for both the whole-buffer and streaming decoders: the complete ISO-8859 family (parts 2 through 16), Windows-1250/1251/1253/1254/1255/1256/1257/1258, and KOI8-R/U. Most are vendored verbatim from the authoritative unicode.org mapping files; ISO-8859-5/9/15 are derived exactly; Windows-1254 composes from Windows-1252. Multi-byte CJK encodings (#99) are tracked separately.

Fixed

  • XSD identity-constraint XPath compile errors are no longer swallowed (#111): a selector or field XPath that fails to compile is reported once up front as a located error ("identity constraint 'k': invalid field XPath '...'"), instead of try? silently turning a schema author's typo into a disabled constraint. The broken query still evaluates as no-match afterwards, so the rest of the document is validated.
  • XSD type resolution no longer accepts invalid input silently (#110): an xsi:type naming an undeclared type is now a located error ("unknown xsi:type '...'") instead of a silent fallback to the declared type, in both the tree and streaming validators; and typeReference chains are resolved through one shared cycle-detecting resolver, so a circular chain reports "circular type reference '...'" instead of silently truncating after 64 hops (and no longer recurses without bound in the streaming shallow check, a latent crash). Completions use the same resolver, best-effort.
  • The HTML fragment parser (HTML.parse) now reconstructs active formatting elements, matching the document parser and the HTML5 algorithm (#109, surfaced by the Tier-2 conformance corpus). When a formatting element is closed out of order it is re-opened for the following content, so <b><i></b>X</i> yields <b><i></i></b><i>X</i> (not <b><i></i></b>X), in both the fragment and document paths.
  • XSD identity-constraint errors now locate the offending field, not just the element (#104): a single-field key/keyref/unique error extends the coding path with the field itself, an attribute (... at path: list/item[2]/@id) or a child element (.../code), resolved from the constraint's field XPath. keyref scope resolution is confirmed to resolve a key declared on an ancestor element (cross-scope), with a test. Full XSD qualified-node-set conformance for pathological multi-level same-name nesting remains part of the conformance-corpus work.
  • XSD identity-constraint errors (key, keyref, unique) now carry a coding path locating the actual offending element, instead of always rendering "at root of document". The IdentityValidator walk threads a [PathKey] the same way the content-model validator does, and each error is located at the selected target node, so a duplicate-key failure reports "... at path: list/item[2]" and a dangling keyref "... at path: orders/line" rather than an empty path. This brings the identity-constraint rule into line with the validation framework's path-carrying contract, which every other rule already honored. The "no element declaration for the root" XSD error is likewise located at the root element, matching the namespace check beside it.
  • A <template> element's flow content is now kept nested inside it (<template><div>x</div></template>) instead of being split out into the body (#83). template is built as body flow content rather than routed to the head.

Added

  • ISO-2022-JP decoding (#108, in progress): the byte decoder gains the WHATWG iso-2022-jp algorithm, selected by the iso-2022-jp declaration name. Unlike the other CJK encodings this one is stateful, escape sequences switch between ASCII, JIS X 0201 Roman (with the yen sign and overline), JIS X 0201 katakana, and the JIS X 0208 plane, which is resolved through the shared jis0208 table (no new data). The streaming decoder now honors the declared encoding from the XML declaration (the libxml2 behavior), not only a byte-order mark, so the single-byte and multi-byte CJK decoders, including the stateful ISO-2022-JP whose mode is carried across reads, work over an incremental byte source; a test asserts streaming agrees with whole-buffer decode for a single-byte, a CJK, and an ISO-2022-JP document.
  • EUC-TW decoding (#108): the byte decoder gains EUC-TW (CNS 11643), selected by the euc-tw declaration name, for both the whole-buffer and streaming decoders. ASCII passes through; a lead in 0xA1-0xFE opens a two-byte plane-1 character; 0x8E opens a four-byte form whose second byte selects the plane. CNS planes 1 and 2, which carry essentially all real-world EUC-TW text, are vendored from the ICU euc-tw-2014 mapping (private-use fallbacks dropped); the rarely used planes 3-15 decode to the replacement character and are a documented follow-up.
  • Streaming (reader-driven) DTD validation (#107, in progress): PureXML.validate(streaming:dtd:) and PureXML.Validation.StreamingDTDValidator validate a document against a DTD while it is pulled event by event (the libxml2 xmlTextReader validation model), holding only the open-element stack rather than the whole tree. Each element's content model and attributes are checked the moment it closes, by applying the exact per-element DTD rules to a shallow element synthesized from the streamed child names. Document-scoped ID/IDREF integrity is tracked in bounded sets (the identifier values, not the tree): a duplicate ID is reported at its repeat occurrence, and forward IDREFs resolve at the end of the stream. Streaming and tree validation report the same problems (a test asserts matching reasons), and errors carry a libxml2 xmlGetNodePath-style coding path. RELAX NG streaming is also available via RelaxNG.validate(streaming:): the Brzozowski derivative is itself incremental, so it is driven directly from the event stream, retaining only the residual pattern (the after-threaded chain is the validation stack) and a light per-element frame for ignorable-whitespace handling; a differential test asserts it agrees with the tree engine across valid, invalid, whitespace, and mixed-content documents. XSD streaming is available via Schema.Document.validate(streaming:): each element's type is resolved from its parent's content model (and xsi:type / typeReference) as it opens, and its attributes and content-model structure are checked shallowly as it closes (ComplexValidator.validateShallow), with a differential test asserting agreement with the tree validator. Wildcard processContents and substitution-group resolution in the streaming form stay on the tree validator.
  • Output encoding (#106): Serializer.serialize(_:encoding:) and PureXML.serialize(_:encoding:) now emit bytes in a chosen encoding (the libxml2 xmlSaveToFilename(..., encoding) model), not only UTF-8. A new PureXML.Parsing.ByteEncoder covers the Unicode transformation formats, the single-byte legacy families (ISO-8859, Windows code pages, KOI8), and the multi-byte CJK encodings (Shift-JIS, EUC-JP, EUC-KR, GBK, GB18030, Big5). The CJK and single-byte inverses are built by running the existing, tested decoders over every valid byte sequence and recording the shortest sequence per scalar, so each encoder is by construction the exact inverse of its decoder (no new data, no separately maintained encode tables); GB18030's astral range uses the four-byte pointer formula. The declaration carries the encoding's canonical name, a UTF-16/32 stream is preceded by a byte-order mark, and any scalar the target encoding cannot represent is written as a decimal numeric character reference, so output always round-trips.
  • Big5 (#103): the byte decoder gains the WHATWG big5 algorithm, for both the whole-buffer and streaming decoders, selected by the big5 declaration name (and the big5-hkscs / cn-big5 aliases). A lead in 0x81-0xFE followed by a trail in 0x40-0x7E or 0xA1-0xFE resolves through the index, covering Traditional Chinese and the HKSCS extensions including the astral CJK ideographs (Extension B and the Compatibility Supplement). Four pointers decode to a base letter plus a combining mark (a two-scalar sequence). The 18,590-entry index is vendored verbatim from the WHATWG index-big5.txt. Big5 completes the major CJK families libxml2 reaches through iconv (Japanese, Korean, Simplified and Traditional Chinese).
  • Full GB18030 (#102): the byte decoder gains the GB18030 four-byte sequences on top of the GBK two-byte subset, for both the whole-buffer and streaming decoders, selected by the gb18030 declaration name. A lead in 0x81-0xFE followed by a digit opens a four-byte form whose pointer is mapped through the WHATWG index-gb18030-ranges linear ranges, reaching the supplementary planes (the 189000 range maps to U+10000 and beyond). The 207 ranges are vendored verbatim from the WHATWG index-gb18030-ranges.txt. GB18030 is China's mandatory standard, supported in libxml2 through iconv.
  • CJK multi-byte encodings (#99, in progress): the byte decoder gains a multi-byte path and the WHATWG Shift-JIS and EUC-JP algorithms, for both the whole-buffer and streaming decoders, selected by the XML declaration's encoding name. Shift-JIS covers ASCII, half-width katakana, and the JIS X 0208 plane; EUC-JP adds the 0x8E half-width katakana and 0x8F JIS X 0212 leads. EUC-KR (CP949/UHC) decodes Hangul and Hanja through the WHATWG index-euc-kr, and GBK (which also covers GB2312) decodes Chinese through the two-byte range of index-gb18030 with 0x80 as the euro sign. The JIS X 0208/0212, CP949, and GBK indexes are all vendored verbatim from the corresponding WHATWG index-*.txt files. This completes the CJK multi-byte encodings.
  • HTML named character references: the complete WHATWG named character reference set (#84), all 2,125 semicolon-form references including the astral mathematical alphanumerics and the 93 two-codepoint references, vendored verbatim from the authoritative entities.json (replacing the previous 16-entry stopgap). Decoding stays longest-match with an optional trailing semicolon, on top of the existing numeric-reference validation.
  • HTML5 select content model (#83, stage 10): a <select> now follows the in-select rules. <optgroup> closes an open <option>, a nested <select> closes the open one rather than nesting, and an <input>/<keygen>/<textarea> closes the select and is placed after it. In the "in select in table" case, a table-structural tag (caption/table/tbody/tfoot/thead/tr/td/th) closes a select that is inside a table before being processed.
  • HTML5 frameset documents (#83, stage 8): a <frameset> after the head now produces a frameset document whose frameset element replaces the body (<html><head></head><frameset>...), with nested framesets and <noframes> handled, and <frame> is now a void element (it no longer wrongly nests). An ordinary document still produces a body.
  • HTML5 integration points (#83, stage 7): HTML content inside an SVG integration point (foreignObject, desc, title) is now parsed in the HTML namespace rather than SVG, so <svg><foreignObject><div> puts the div (and its descendants) back in HTML; re-entering <svg> inside an integration point switches back to the SVG namespace.
  • HTML5 table foster-parenting (#83, stage 6): stray flow content inside a table is now moved out rather than nested in the table structure, matching the HTML standard. A non-table element or character data appearing while a table/tbody/thead/tfoot/tr is the open node is inserted immediately before the table (<table><b>x</table> -> <b>x</b><table></table>), while well-formed cell content is untouched.
  • HTML5 SVG attribute-name case adjustment (#83, stage 5): an SVG attribute written in mixed case is restored to its canonical camel case in the tree (viewbox -> viewBox, gradienttransform -> gradientTransform, and the rest), applying only to elements in the SVG namespace. HTML attribute names are left as the tokenizer lowercased them. With this, SVG/MathML foreign content carries correct element and attribute names.
  • HTML5 adoption agency algorithm and active formatting elements (#83, stage 4): misnested formatting tags are now recovered exactly as the HTML standard prescribes. Overlapping tags nest (<b><i></b></i> -> <b><i></i></b>), content after an out-of-order close is re-wrapped by reconstructing the active formatting elements (<b><i></b>X</i> -> <b><i></i></b><i>X</i>), and a block inside a formatting element reparents through the furthest-block path, including the canonical <p>1<b>2<i>3</b>4</i>5</p> -> <p>1<b>2<i>3</i></b><i>4</i>5</p>.
  • HTML5 SVG element-name case adjustment (#83, stage 3): an SVG element written in mixed case (which the tokenizer lowercases) is restored to its canonical camel case in the tree, so <foreignObject>, <linearGradient>, <clipPath>, and the rest carry their correct SVG names. Applies only in the SVG namespace; same-named HTML elements keep their lowercased name.
  • HTML5 foreign content (#83, stage 2): an <svg> or <math> element and all of its descendants now carry the SVG (http://www.w3.org/2000/svg) or MathML (http://www.w3.org/1998/Math/MathML) namespace on their QualifiedName, so foreign elements are distinguishable from same-named HTML; ordinary HTML content (including content after a closed <svg>) stays in no namespace. (SVG/MathML element- and attribute-name case adjustment, and HTML integration points, are a later refinement.)
  • HTML5 table tree construction (#83, stage 1): a <table> written without its section and row wrappers now nests correctly. A <tr> directly inside a <table> gets an implied <tbody>, a <td>/<th> gets an implied <tr> (and section), consecutive rows share one implied <tbody>, and an explicit <thead>/<tbody>/<tfoot> is not duplicated, all inside an open table only.
  • HTML tokenizer correctness (#84, partial): numeric character references are now range/surrogate-validated (zero, surrogates, and values above U+10FFFF yield U+FFFD) with the Windows-1252 C1 fixup table (&#x80; -> euro, etc.); named references are decoded by longest match with or without a trailing semicolon; a literal NUL byte becomes U+FFFD; RCDATA elements (title, textarea) decode character references while raw-text elements (script, style) take their content verbatim; and a single leading newline after <textarea> is stripped. (The full ~2,000-entry named-entity table remains deferred to vendoring from the authoritative WHATWG data, the same policy as the legacy encodings.)
  • HTML conformance validator in the composable Validation idiom. PureXML.HTML.validationErrors(in:) (backed by PureXML.Validation.HTML) judges a parsed tree against the intrinsic HTML5 content-model invariants as named, removable, isolation-testable Validation rules, the same idiom as the DTD/XSD/structural validators: voidElementsAreEmpty (void elements carry no content), requiredParent (li/td/option/... appear inside an allowed parent, with the allowed parents named in the finding), and uniqueIdentifiers (id values are unique). Failures are located ValidationErrors; this is the validation layer HTML previously lacked, so every parsing/schema subsystem now exposes its conformance checks as composable rules.
  • XInclude range inclusion (#87), completing the XInclude audit. An xi:include whose xpointer selects a range (xpointer(range(...)), range-to, or string-range) now includes that range's content: when no node-selecting scheme matches, the processor falls back to the XPointer range model and substitutes each range's spanned nodes (or the matched text for a string-range).
  • XPointer range() model (#88), completing the XPath/XPointer audit. PureXML.XPointer.evaluateRanges(_:over:) (and Pointer.evaluateRanges(over:)) evaluate the range forms inside an xpointer()/xpath1() scheme: range(expr) (the covering range of each location), start/range-to(end) (one range spanning from the first location of start to the first of end, the sibling run when they share a parent), and string-range(location, "search"[, offset[, length]]) (a character range per occurrence, with the XPointer offset/length adjustment). Each Range materializes to the whole nodes it spans and the text it covers, and xmlns() bindings apply to the range parts that follow, so this also unblocks XInclude range inclusion (#87).
  • C14N node-subset canonicalization and Canonical XML 2.0 parameters (#85), completing the canonicalization audit. Canonicalizer.canonicalize(_ subtree: TreeNode) canonicalizes a fragment in place: the apex element renders the namespace context in scope from its omitted ancestors and inherits their xml:base/xml:lang/xml:space when it does not set them, so a signed fragment is independent of where it sat. canonicalize(_:including:) canonicalizes a node-set selected by a TreeNode predicate (XPath/position-based selection): an excluded element's tags are omitted but its selected descendants are kept and re-declare their in-scope namespaces. Options.prefixRewrite = .sequential rewrites every prefix to a canonical n0/n1/... in document order (eliminating the default namespace and placing declarations at first use), and Options.qnameAwareLabels extends that rewrite into QName-valued attributes and elements, so documents differing only in prefix spelling canonicalize to identical bytes.
  • Serialization textEscaping round-trip option (#91), completing the serialization audit. Options.textEscaping defaults to .standard (escape only &, <, >); .roundTrip also escapes a carriage return as &#xD; in text and CDATA-as-text output, so a literal \r survives the parser's end-of-line normalization and the document round-trips byte for byte. (The CDATA-section vs escaped-text toggle, cdataAsText, was already present.)
  • DOM tree: first-class doctype, entity-reference, and namespace nodes, plus ownerDocument, adopt, and importNode (#90), completing the tree-model audit. TreeNodeKind gains .doctype, .entityReference, and .namespace, each with a factory (TreeNode.doctype(name:publicID:systemID:internalSubset:), .entityReference(_:children:), .namespace(prefix:uri:)) and typed accessors; doctype and namespace nodes drop out of the content value-Node projection while an entity reference splices its replacement into place. ownerDocument derives the owning document from the parent chain; adopt and importNode move or copy a subtree into another document, re-declaring every namespace it relied on from an outer scope so the result is self-contained.
  • Schematron key(), document(), and current() in tests (#80), completing the Schematron audit. Test, let, and message expressions can now resolve nodes through xsl:key declarations, load an external document for cross-document checks (via an injected documentLoader), and refer to the rule context node with current().
  • RELAX NG compact >> follow-annotations (#79), completing the RNC syntax. A >> annotation after a pattern (a foreign element name with optional [ … ] content) is skipped, chaining and combining with leading [ … ] annotations; both carry no schema semantics. (Namespace declarations inside annotations are already dropped by the annotation skipper.)
  • XSLT document() fragments and base-URI resolution (#82), completing the XSLT function/output audit. A #fragment selects a subset of the loaded document via XPointer, a node-set first argument unions the documents named by each node, and a relative URI is resolved against a caller-supplied baseURI (also applied to xsl:include/xsl:import).
  • XSLT result-tree fragments usable as node-sets (#82). An xsl:variable with a body now binds a queryable document fragment instead of a plain string, so count($rtf/*), $rtf/child, and xsl:for-each select="$rtf/..." work; it still has its concatenated text as a string value.
  • XSLT xsl:output cdata-section-elements (#82). The text content of the named result elements is emitted in <![CDATA[…]]> sections instead of escaped, leaving nested elements and other elements unaffected.
  • XSLT xsl:output doctype-public/doctype-system (#82). When either is set, a <!DOCTYPE> for the result's root element (PUBLIC when both are given, SYSTEM otherwise) is emitted before the serialized output, for XML and HTML methods.
  • XSLT key() over a node-set second argument (#82). key(name, node-set) (including key('k', .)) now unions the matches for every node's string value and de-duplicates, rather than using only the first node's string value.
  • XSLT method="html" output (#82). An xsl:output method="html" result is now serialized by the HTML rules (void elements like <br> emitted without a self-closing slash, raw-text elements like <script>/<style> left unescaped) instead of as XML.
  • XSLT xsl:sort case-order (#82). When case-order="upper-first"/"lower-first" is set, the sort comparison is case-insensitive with the case used only to break ties among otherwise-equal strings; without it the default codepoint order is kept.
  • XSLT functions generate-id, system-property, element-available, function-available, and an unparsed-entity-uri stub (#82). generate-id returns a stable per-node identifier (the context node when no argument is given, the empty string for an empty node-set); system-property reports the XSLT version (1.0) and vendor; the *-available functions answer against the set of implemented instructions and functions.
  • XSLT xsl:namespace-alias (#81), completing the XSLT top-level-element audit. A literal result element (and its attributes) in a stylesheet namespace bound by stylesheet-prefix is rewritten on output to the namespace and prefix bound by result-prefix (#default supported), so a stylesheet can emit literal xsl: elements. Folded across xsl:include/xsl:import.
  • XSLT xsl:decimal-format (#81). format-number now honors named and default decimal formats: the decimal separator, grouping separator, percent, minus sign, zero-digit (so non-Latin digit sets render), NaN, and infinity symbols are taken from the chosen xsl:decimal-format, both in the picture and the output. Folded across xsl:include/xsl:import.
  • XSLT xsl:fallback (#81). An unrecognized XSLT element instantiates its xsl:fallback children in its place (forwards-compatible processing), or is dropped when it has none; a fallback under a supported instruction is ignored.
  • XSLT xsl:apply-imports (#81). Re-applies templates to the current node in the current mode, considering only templates of lower import precedence than the one being instantiated (so an overriding template can extend the imported one), falling back to the built-in rule when none match.
  • XSLT xsl:attribute-set (#81). Named attribute sets are applied to a literal result element or xsl:element via use-attribute-sets; sets may include other sets (recursively, with a cycle guard), set attributes are lowest precedence so the element's own attributes override them, and same-named attributes collapse to the last. Folded across xsl:include/xsl:import.
  • XSLT xsl:message (#81). The message body is instantiated as diagnostic text; terminate="yes" aborts the transformation with that text as an XSLTError.terminated, and processing stops at that point. A non-terminating message produces no result-tree output.
  • XSLT xsl:strip-space / xsl:preserve-space (#81). Whitespace-only text nodes are removed from the source tree for elements named by strip-space before transformation, with preserve-space and a more specific name test winning over *, and xml:space="preserve" on a source element or ancestor overriding the stripping. Folded across xsl:include/xsl:import.
  • DTD located content-model errors. An element whose children break its content model now names each stray child individually (element <x> is not allowed in <r>), and reports a pure order/count violation once with the allowed elements as a hint (… do not match its content model; allowed: <a>, <b>), instead of a single opaque message, matching the editor-grade diagnostics of the other validators.
  • XSD located content-model errors. A content-model violation is now placed at the offending child with a recovery hint naming what was expected there (element 'x' is not allowed here; expected <b>, content is incomplete; expected <b>), instead of one opaque "content does not match the content model" per element. An xs:all group recovers past each stray child and reports every missing required member, and well-placed children are still validated for their own content, so an editor sees every problem at once. This brings XSD content validation to the same located, recovering standard as the RELAX NG, DTD, and Schematron validators.
  • RELAX NG located, recovering validation errors (#79). Alongside the boolean validate(_:), RelaxNG.errors(in:) now reports every way a document fails the schema as individual located errors (each with a coding path), recovering past each failure so an editor can surface all of a faulty document's problems at once rather than only the first. Errors carry recovery hints (expected <a>, <b>) naming the element types accepted at the failure point, and distinguish unexpected elements, invalid or missing attributes, invalid text/datatype content, and missing required content. A datatype mismatch quotes the offending value and names the expected type ('abc' is not a valid integer), and a value mismatch quotes both the actual text and the required literal. An attribute failure distinguishes an undeclared attribute from a declared one with a bad value, quoting the value and naming its type. RelaxNG.validation() exposes the schema as a Validation<Node, Void>, so RELAX NG composes with the same validation framework as the XSD, DTD, and Schematron rules. The boolean engine remains the authority on validity; the walk only places and explains the failures.
  • XML catalog group-level prefer (#86). A prefer attribute on a group (or a nested catalog) now overrides the catalog-wide preference for the public entries inside it, inherited through nested groups. Resolution of an external identifier consults each public entry's own effective preference, so two entries in one catalog can prefer differently. A system match still always wins.
  • Schematron abstract patterns (#80). A <pattern abstract="true"> is a template whose rule queries carry $name references; a <pattern is-a="…"> with <param name= value=> children instantiates it, substituting each parameter into the template's context, test, <let> value, and <value-of select=> in a single pass that never rescans a substituted value or lets a short name capture a longer one. The template itself contributes no rules, and $name references that are not parameters (ordinary <let> variables) are preserved.
  • RELAX NG conformance: datatypeLibrary, value-space <value>, name-class subtraction, and compact-syntax breadth (#79). The in-scope datatypeLibrary is now inherited from the nearest ancestor; the default library defines only string and token, the W3C XML Schema library defines the full built-in set, and a type no in-scope library defines is an unknown datatype, so its <data> or <value> matches nothing rather than validating silently. A <value> carries its datatype and compares in that type's value space, so 1 equals 01 and 1.5 equals 1.50, booleans treat 1/true and 0/false as equal, and string/token compare by their whitespace-normalized form. A new nsNameExcept name class backs <nsName> with <except> and the compact prefix:* - name subtraction. The compact parser also skips [ … ] annotations, treats div { … } as transparent grouping, and folds ~ string concatenation.
  • XSLT template parameters and modes (toward full parity, #61). xsl:param declarations (with defaults), xsl:with-param on apply-templates and call-template, and mode on templates and apply-templates are now honored: a passed parameter overrides the declared default, and a mode routes nodes only to templates declared in that mode (the built-in rules recurse in the same mode).
  • Push / feed streaming API (#1), the last epic. A resumable scanner reports a token only once its terminator is fully buffered and otherwise signals need-more (the Expat XML_TOK_PARTIAL model), so PureXML.Parsing.PushParser parses input fed in arbitrary chunks with feed(_:)/finish(), driving a SAXHandler, while retaining only the current incomplete token plus the open-element stack. PureXML.events(feeding:) exposes an AsyncThrowingStream of events over any async sequence of text chunks. Splitting a document at any byte boundary, down to one character per feed, yields identical events.
  • HTML parser and serializer (#20, the libxml2 HTMLparser.h/HTMLtree.h model). PureXML.HTML.parse reads tag-soup HTML leniently (case-insensitive tags; quoted, unquoted, and boolean attributes; comments; doctype; character references) and builds a node tree handling void elements (no end tag), optional end tags (implicit close of li, p, table rows and cells, and the like), raw-text elements (script, style), and unmatched end tags. PureXML.HTML.serialize writes it back with void elements unclosed and raw-text content unescaped.
  • XSLT 1.0 transformation engine, completing #3. PureXML.XSLT.transform applies a stylesheet to a source document by the XSLT processing model: it matches each node to the highest-priority template (or the built-in rules) and instantiates the template's sequence constructor. Implements apply-templates (with template dispatch and sort), value-of, for-each, if, choose, element, attribute, copy, copy-of, call-template, variable, literal result elements, and attribute value templates, all over the existing XPath engine. The identity transform round-trips a document including its attributes.
  • XSLT 1.0 stylesheet model and parser (toward #3). PureXML.XSLT gains a stylesheet model (template rules with match patterns and computed default priorities, the instruction sequence constructor, attribute value templates) and a parser that compiles an xsl:stylesheet document into it. Recognizes the XSLT vocabulary by namespace or xsl prefix; other elements are literal result elements. Supports template, apply-templates, value-of, for-each, if, choose, element, attribute, copy, copy-of, call-template, variable, text, and sort.
  • Schema conformance fixtures, completing XSD and RELAX NG validation (#2). A cross-cutting suite validates realistic schemas (a patterned, bounded, ID-keyed XSD purchase order and a RELAX NG contact card using interleave, attribute value choices, and optional/repeatable elements) against conforming and non-conforming instances, plus datatype boundary fixtures.
  • RELAX NG validation by derivatives (toward #2). PureXML.Schema.RelaxNG parses a RELAX NG schema in the XML syntax into a pattern grammar (element, attribute, text, empty, notAllowed, group, choice, interleave, optional, zeroOrMore, oneOrMore, mixed, list, ref/define, data, value, and the name classes), and validate(_:) checks an instance document by James Clark's derivative algorithm: the pattern is derived through each start-tag, attribute, text, and end-tag event, and the document is valid when the residual pattern is nullable. Handles interleave, recursive grammars, and reuses the XSD datatype library for data/value.
  • XSD schema-document parser, completing the XSD validation pipeline (toward #2). PureXML.Schema.Document(xsd) compiles a schema document into its global element declarations and named-type table, and validate(_:) checks an instance document end to end. The parser matches the schema vocabulary by local name and supports global and local element declarations, named and inline simple and complex types, sequence/choice/all model groups with occurrence, attribute uses, simple content (restriction/extension), the full facet set, and recursive types (via named type references resolved during validation).
  • XSD complex types and particles (toward #2 schema validation). PureXML.Schema.ComplexValidator validates an element against a ComplexType: its attribute uses (required and typed, with unknown-attribute rejection), and its content model. Content is empty, simpleContent (text validated against a simple type), elementOnly, or mixed. Particles compile to a Thompson NFA over element names honoring minOccurs/maxOccurs, with sequence/choice compositors; all groups are validated order-independently by counting. Child elements are validated recursively against their declared types (the XSD Element-Declarations-Consistent rule makes the name-to-type map well defined).
  • XSD simple-type datatype library (toward #2 schema validation). The new PureXML.Schema namespace validates a lexical value against the XSD Part 2 built-in datatypes (string, boolean, decimal, the bounded integer family, float/double, duration, the eight date/time types, hexBinary, base64Binary, anyURI, QName, and the Name/NCName/NMTOKEN/language family) and the constraining facets (length/minLength/maxLength, pattern via the regex engine, enumeration, whiteSpace, min/maxInclusive/Exclusive, totalDigits, fractionDigits). Decimals are compared exactly with no floating-point loss, date/time fields are range-validated (leap years, 24:00:00, timezone bounds), and the integer types enforce their intrinsic bounds.
  • Regular-expression engine (#30, the XML Schema regex flavor, gating XSD). The new PureXML.Regex namespace compiles a pattern (literals, ., character classes with ranges and negation, the escapes \d \D \w \W \s \S \i \I \c \C, single-character escapes, grouping, alternation, and the quantifiers ?, *, +, {n}, {n,}, {n,m}) to a Thompson NFA and matches the whole string, as xs:pattern facets require. Matching is linear in the input with no backtracking blow-up. \p{...} Unicode category escapes are not yet supported.
  • Canonical XML (#26, the libxml2 c14n.h model). The new PureXML.Canonical namespace renders the C14N form of a node: namespace declarations sorted by prefix (default first) and attributes by namespace URI then local name, empty elements expanded to start/end pairs, CDATA folded into escaped text, and the C14N text/attribute escaping rules. Inclusive mode renders every in-scope namespace; exclusive mode renders only the namespaces an element and its attributes visibly use (with an InclusiveNamespaces prefix list). Comments are omitted unless requested. For whole-tree canonicalization C14N 1.0 and 1.1 coincide, so the single inclusive mode covers both.
  • XInclude, URI resolution, and xml:base (#24, the libxml2 xinclude.h/uri.h model). The new PureXML.XInclude namespace adds RFC 3986 reference resolution (URIReference.resolve(_:against:), with dot-segment removal) and process(_:base:loadingURI:), which replaces xi:include elements with their targets. hrefs resolve against the supplied base and the in-scope xml:base, parse="xml" includes the document element (or an xpointer() fragment via PureXML.XPointer), parse="text" includes raw text, and a failed load uses xi:fallback or errors. Includes nest. PureXML does no I/O itself: fetching is only through the injected loadingURI closure, so the default fetches nothing.
  • XML Catalog resolution (#27, the libxml2 catalog.h model). The new PureXML.Catalog namespace parses an OASIS XML catalog (matching public, system, uri, rewriteSystem, rewriteURI, and group/catalog by local name) into a Resolver that maps public/system identifiers and URI names to replacement URIs, with longest-prefix rewriting and system-over-public precedence. Resolver.entityResolver(loadingURI:) builds a Parsing.EntityResolver that resolves an external identifier through the catalog and then loads the URI via an injected closure, so a catalog plus a loader wires external entities and DTDs in while the default (no loader) keeps XXE closed.
  • Schematron validation (#25, the libxml2 schematron.h model). PureXML.Validation.Schematron(schema:) compiles a Schematron schema (namespace-agnostic over the ISO and legacy namespaces) and validate(_:) checks a document: each <rule>'s context XPath selects the nodes it fires on, and each <assert>/<report> test is evaluated relative to that node, a failed assert becoming an error issue and a matched report a warning. Within a pattern a node fires the first matching rule. Built on the XPath context-node evaluation, with a new XPath.Query.nodes(over:) that returns matched tree nodes for repeated querying of one tree.
  • XPointer (#23, the libxml2 xpointer.h schemes). The new PureXML.XPointer namespace resolves the shorthand bare-name form (name = id('name')), the element() scheme (child-element positions from an id or the document root, e.g. element(intro/2), element(/1/2)), and the xpointer() scheme (a full XPath expression). A pointer may chain scheme parts; they are tried in order and the first non-empty selection wins (the fallback rule). Built on PureXML.XPath.
  • Streaming pattern matching (#22, the libxml2 pattern.h model). The new PureXML.Pattern namespace compiles the streamable XPath subset (element names, *, prefix:*, /, //, a leading absolute /, and a trailing attribute step) into a Matcher, rejecting predicates and ./.. as outside the subset. PureXML.Pattern.matches(_:in:) streams a document through the pull parser and returns the paths of matching nodes (/a/b/c, /a/@id) in document order, deciding each match from the open-element stack alone, so no tree is built. Matcher.matchesElement(path:) reuses a compiled pattern.
  • The XPath evaluation context, completing full XPath 1.0 (#21). Query.value(at:position:size:variables:) evaluates an expression against an explicit context node already in a tree, with its proximity position, size, and variable bindings, so position(), last(), and the upward axes resolve from that node. This is the per-node entry point downstream engines (XSLT, Schematron) drive.
  • The full XPath 1.0 function library (toward #21 full XPath 1.0). Beyond the core functions, the string family (concat, starts-with, contains, substring-before, substring-after, substring, string-length, normalize-space, translate), the number family (sum, floor, ceiling, round), and the node family (local-name, name, namespace-uri, id, lang). All pure Swift with no Foundation. Without a DTD, id() matches by an attribute named id or xml:id.
  • The XPath expression engine and four-type model (toward #21 full XPath 1.0). PureXML.XPath.Query now compiles and evaluates full expressions, not just location paths: the operator grammar (or, and, =, !=, <, <=, >, >=, +, -, *, div, mod, unary -, |) with correct precedence, number and string literals, function calls, variable references, and filter expressions, all over PureXML.XPath.Value (node-set, boolean, number, string) with the spec's coercion rules and canonical number formatting. Predicates are now arbitrary expressions (a numeric predicate is a position test). Query gains value(over:variables:), number, string, and boolean accessors, and a binding map for $variables. The core functions last, position, count, not, true, false, boolean, number, and string ship with it.
  • All thirteen XPath axes (toward #21 full XPath 1.0). PureXML.XPath.Query now navigates child, descendant, parent, ancestor, following-sibling, preceding-sibling, following, preceding, attribute, namespace, self, descendant-or-self, and ancestor-or-self, with the ./../@/// abbreviations and the processing-instruction() node test. Evaluation runs over the parent-aware tree, so upward and sibling navigation are first-class; results come back de-duplicated and in document order regardless of axis direction. The attribute axis excludes xmlns declarations and the namespace axis surfaces in-scope namespace nodes.
  • Pull-cursor reader (the libxml2 xmlTextReader model). PureXML.Parsing.TextReader (via PureXML.reader(_:)) walks a document one node at a time with read(), exposing nodeKind, name, value, depth, attributes/attributeCount/attribute(_:), and isEmptyElement at each step. It layers on the streaming core, so it never holds the whole document. A childless element is reported once as an empty element (no separate end node), normalizing <a/> and <a></a>. documentType surfaces the DTD read so far as the validation hook.
  • Mutable, parent-aware document tree (the libxml2 tree.h model). PureXML.Model.TreeNode is a reference type that knows its parent and siblings, so a document can be navigated upward and sideways (parent, nextSibling, previousSibling, ancestors, root, elementChildren, stringValue) and edited in place (append, insert(before:), insert(after:), removeFromParent, replace(with:), copy). Children are held strongly and the parent weakly; attaching a node detaches it from any previous parent, and a node can never become its own ancestor. parseTree(_:) builds one from XML and TreeNode.node converts back to the value tree for serialization.
  • External and parameter entities (the libxml2 entities.h model), secure by default. Internal parameter entities (<!ENTITY % name "value">) are stored and expanded within the DTD, including bare %name; references that inject markup declarations, bounded by depth and the expansion budget. External general entities and the external DTD subset are recorded but never fetched by PureXML itself: they are loaded only through an injected PureXML.Parsing.EntityResolver (a struct of closures). The default resolver refuses every external reference, so XXE stays closed; an external entity then fails as undefined and the external subset is left unread. Internal declarations win over the external subset.
  • Serialization option parity for the XML declaration (the libxml2 save-option model). PureXML.Emitting.Options gains includeXMLDeclaration, xmlVersion, encodingName, and standalone; when requested, both the tree serializer and the incremental writer prepend an <?xml ...?> declaration from a single shared computation. Writer.writeStartDocument() emits it. Defaults stay off so fragment output is unchanged.
  • Spec-exact XML character classification (PureXML.Parsing.XMLCharacter, the libxml2 chvalid.h parity): the XML 1.0 (Fifth Edition) Char, NameStartChar, and NameChar productions as public predicates over Unicode scalars, plus isValidName(_:). The scanner's name recognition now uses these exact ranges (replacing a loose isLetter check) from a single source of truth, and handles both precomposed and combining-mark names.
  • Full encoding support. Byte input now decodes UTF-32 (both byte orders, BOM or sniffed) and the single-byte encodings ISO-8859-1 and Windows-1252 selected by the XML declaration's encoding name, in addition to UTF-8 and UTF-16. The streaming byte path also decodes UTF-32. Standard library only, no Foundation.
  • Incremental XML writer (PureXML.Emitting.Writer, the libxml2 xmlTextWriter model): emit a document with start/end and write calls without building a tree. Escaping matches the tree serializer (extracted into a shared Escaping helper), so compact output is byte-identical to serializing the equivalent tree.
  • SAX2 push-callback parsing (the libxml2 SAX2 model). PureXML.Parsing.SAXHandler is a struct of optional callbacks (start/end document, start/end element, characters, CDATA, comment, processing instruction); PureXML.parse(_:sax:) drives them from the streaming core. Element names are namespace-resolved, so the URI is available on each callback.
  • DTD ID/IDREF validation. Attributes typed ID must be unique across the document; IDREF and IDREFS values must each resolve to a declared ID (forward references included, via a second pass). Completes the structural DTD validation layer alongside content models and attribute rules.
  • DTD attribute validation (<!ATTLIST>). The parser surfaces attribute declarations alongside element models, and DTDSchema now checks #REQUIRED attributes are present, #FIXED attributes match when present, and enumerated attributes take a declared value. Tokenized types (ID/IDREF/NMTOKEN) are parsed but their value rules (uniqueness, references) are not yet enforced.
  • DTD content-model validation. The parser now surfaces the <!DOCTYPE> internal subset's <!ELEMENT> declarations (PureXML.Parsing.DocumentType, via Parser.parseWithDocumentType), and PureXML.Validation.DTDSchema validates a tree against them: EMPTY, ANY, (#PCDATA), mixed content, and element content models (sequence/choice with ?/*/+, matched as a regular language over child element names). PureXML.validateAgainstInternalDTD(_:) parses and validates in one call. <!ATTLIST> validation is not yet covered.
  • XPath query support (a practical subset). PureXML.XPath.Query compiles a location path and evaluates it over a node; PureXML.xpath(_:over:) is the one-shot convenience. Supports the forward axes (child, descendant //, self ., attribute @), the node tests (name, *, text(), node(), comment()), and the predicates [n], [@a], [@a='v'], [child], [child='v']. Returns a Selection node-set with element and stringValue accessors. Upward/sibling axes and the full expression language are out of scope (the model has no parent pointers).
  • Optional DTD with internal general entities, off by default. With Limits(allowDoctype: true) the parser reads a <!DOCTYPE> internal subset, honors internal <!ENTITY name "value"> declarations, and expands them in text and attribute values. Defenses are mandatory: expansion is bounded by Limits.maxEntityExpansion (billion-laughs amplification raises ParseError.amplificationLimitExceeded), recursive entities raise ParseError.recursiveEntity, undeclared entities raise ParseError.undefinedEntity, and external entities are never loaded (XXE stays closed). The default still rejects <!DOCTYPE> outright.
  • Streaming byte decode. PureXML.parse(pullingBytes:) and events(pullingBytes:) accept an incremental byte source (() -> UInt8?) and decode UTF-8 or UTF-16 on the fly, so the bytes are never fully buffered. Encoding is detected from the leading bytes; invalid sequences become the Unicode replacement character rather than failing the stream.
  • Namespace resolution. Qualified names now carry an optional resolved namespaceURI, populated by the parser from in-scope xmlns declarations at the start-element boundary (the libxml2 SAX2 model). A default namespace applies to unprefixed element names but not attributes, the xml prefix is built in, xmlns declarations are preserved as attributes for round-trip, and an unbound prefix raises ParseError.undefinedNamespacePrefix. Namespace-free documents are unchanged (URIs stay nil).
  • Byte input with encoding detection. PureXML.parse(bytes:) and PureXML.events(bytes:) accept raw [UInt8] and detect the encoding (UTF-8 or UTF-16, with or without a byte-order mark) following the XML sniff order, then decode with the Swift standard library (no Foundation). PureXML.Parsing. InputEncoding.detect(_:) exposes the detector. Malformed input (such as an odd-length UTF-16 stream) raises ParseError.malformedEncoding.
  • Configurable, bounded-by-default parser limits (PureXML.Parsing.Limits): maximum nesting depth (default 256), name length, and content length, enforced during scanning. Protects against pathological input and keeps the recursive node model safe to build and tear down. Threaded through parse/events.
  • Initial package scaffold: root Swift package, namespacing under PureXML, and the portability and verification gates carried over from PureYAML.
  • XML node model (PureXML.Model): Node, Element, Attribute, and QualifiedName, preserving document order and node kinds.
  • XML emitter (PureXML.Emitting.Serializer) with pretty-printed and compact output and correct text and attribute escaping.
  • Structural validation (PureXML.Validation.Validator) for duplicate attribute names.
  • Streaming, iterative XML parser. PureXML.Parsing.EventReader is a pull-based event core that consumes input through a character-source closure and emits one Event at a time, holding only bounded state (an open-element stack and a small lookahead buffer). It never requires the whole document in memory and can drive chunked input. PureXML.Parsing.Parser builds a Model.Node tree iteratively over the event core (no recursion). Handles elements, attributes, text, the five predefined entities and numeric character references, comments, CDATA, and processing instructions. Safe by default: <!DOCTYPE> is rejected (no DTD/XXE).
  • Top-level API: PureXML.parse(_:), PureXML.parse(pulling:), and PureXML.events(_:) / PureXML.events(pulling:) for streaming.

Changed

  • The emitter is now iterative (explicit work stack, not recursion), mirroring libxml2's approach, so deep trees do not overflow the call stack.
  • The emitter escapes tab/newline/CR (and >) in attribute values so attributes round-trip through spec-compliant parsers; mixed content is no longer reformatted (formatting is suppressed for elements with text/CDATA children).