Commit Graph

633 Commits

Author SHA1 Message Date
83e6e27d12 style(format): adopt --importgrouping testable-bottom from canonical rules-swift
Group @testable imports at the bottom, per public/rules-swift's swiftformat. A
behavior-preserving import-ordering reformat across 252 files (mostly tests).
--trailingclosures was deliberately not adopted (cosmetic, and swiftformat flags
it as caution-worthy).
2026-06-18 20:33:07 +02:00
c6f305357c chore(rules): align lint config and pointer with canonical rules-swift
Make public/rules-swift the canonical source (per project direction) and adopt
its opt-in lint rules that PureXML lacked: empty_count, explicit_init, first_where,
sorted_first_last, unused_import, plus its todo disable. All pass with zero
violations on the existing sources.

PureXML keeps its coherent, stricter tuning where the canon is a self-described
'starting point': force_unwrapping/force_try/force_cast = error (safety), and
trailing commas mandated in both swiftformat (--commas always) and swiftlint
(mandatory_comma) to resolve the canon's formatter-adds vs linter-flags mismatch.
docs/rules/README.md now points at public/rules-swift and documents the tuning.
2026-06-18 20:30:46 +02:00
d046189ad6 refactor(schema): extract shared base-component resolution for redefines
The attribute-group and model-group redefine-restriction rules built the
namespaced base-component pool and did the namespaced-key-plus-chameleon-fallback
lookup with duplicated code and near-identical comments. Extract baseComponents(in:
named:) and resolveBaseComponent(_:name:owner:) so the recurring namespace-keying
and chameleon-fallback pattern lives in one place. Behavior-preserving: all redefine
unit tests pass and the XSTS buckets are unchanged (0/48/0/31).
2026-06-18 20:20:26 +02:00
f8ba6fef5e fix(schema): reject a constraining facet on anySimpleType-based simpleContent
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;
it stops at any other built-in, a simple type, or an external/unresolved base, 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.

XSTS invalid-schemas-accepted 49 -> 48, no false positive (valid-schemas-rejected
and valid-instances-rejected held at 0; an adversarial critic probed 12 valid
schemas across every resolver branch and confirmed FP-safety), no other bucket
moved.
2026-06-18 19:27:28 +02:00
afad5f822c fix(schema): reject adding an attribute in an attribute-group redefine
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 for a
no-targetNamespace redefined schema.

False-positive-free: the added-attribute check runs only when the original's
attribute set is fully known from its direct declarations and the redefinition
carries no attributeGroup reference (a self-reference brings in the original's
attributes, so declaring more alongside it is a valid redefine, not an addition,
W3C attgC007/attgC038/groupB018, test75083/xsd003b), and a use=prohibited
attribute the base lacks forbids rather than permits a name, so it does not widen
the usable set and is left alone.

XSTS invalid-schemas-accepted 51 -> 49, no false positive (valid-schemas-rejected
and valid-instances-rejected held at 0; five self-reference over-rejections were
caught by the gate and one prohibited-attribute over-rejection by an adversarial
critic, all fixed before commit), no other bucket moved.
2026-06-18 18:41:13 +02:00
af5d7882b0 fix(schema): enforce model-group redefine restriction (cos-group-restrict)
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
particle-restriction oracle used for complex-type restriction, resolves the
original group by namespaced identity with a {}name chameleon fallback, and runs
post-compile where element type compatibility is available.

Now rejected: adding required content to an empty base (schZ006), dropping a
required all member (schL1), reordering an all (schL5/L6, the Recurse rule is
order-preserving even for all), growing to a superset (schL8), and the
multi-document/chained redefines schN4/5/6/7 and schL10. To stay
false-positive-free the check skips a redefinition carrying a self-reference and
declines to judge a base with a maxOccurs=0 pointless particle (the contested
case W3C accepts, mgO013); genuine restrictions (dropping an optional member, an
unchanged redefinition) are accepted.

XSTS 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, and an adversarial critic confirmed FP-safety),
no other bucket moved. Disclosed under-rejections: self-referencing redefinitions
(schN10) and maxOccurs=0-base redefines.
2026-06-18 18:24:58 +02:00
1d17d2089f fix(schema): reject relaxing a fixed value in an attribute-group redefine
A redefinition of an attribute group restricts the original (src-redefine /
cos-ct-restricts), so it may not relax a fixed value the original sets. schM10
(changing a fixed attribute to a default one) is now rejected. The 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, and it rides the existing namespaced base lookup, so
no valid redefinition is rejected.

XSTS invalid-schemas-accepted 65 -> 64, no false positive (valid-schemas-rejected
and valid-instances-rejected held at 0), no other bucket moved.
2026-06-18 17:57:43 +02:00
be9ba9fc1e fix(schema): a redefined attribute group keeps required and re-prohibits prohibited attributes; invalid-schemas-accepted 67 to 65 (#145, #161)
A redefinition of an attribute group restricts the original (src-redefine /
cos-ct-restricts), so it may neither eliminate a required attribute the original
declares (attgZ002) nor re-introduce a prohibited one as usable (attgZ003).
redefineAttributeGroupRestrictionErrors compares the redefinition to the original
attribute group and reports either violation.

False positives the verification caught and the rule guards against: re-declaring
a base prohibited attribute AS prohibited keeps it prohibited and is valid (the
check requires a non-prohibited use); the drop-required check is skipped when the
redefinition pulls attributes back via a nested attributeGroup reference; and the
base is matched by NAMESPACED identity ({target}name), so a same-local-name
attribute group imported from a different namespace is never mistaken for the base
(the same namespace guard the redefine self-reference rule uses).

invalid-schemas-accepted 67 to 65, no false positive (valid-schemas-rejected and
valid-instances-rejected held at 0). The gate caught a re-prohibition false
positive and an adversarial review caught a cross-namespace collision false
positive; both are fixed and covered by tests.
2026-06-18 17:46:54 +02:00
872e6ea7d0 fix(schema): preserve list/union variety through a simpleContent derivation (#147)
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. simpleContentType built the content type
with only the resolved base and facets, dropping the variety, so a list-typed
simpleContent accepted any whitespace-separated text and an invalid item (e.g. a
non-int in a list of int) was not caught.

A valid list/union value is unaffected; the change only tightens validation of
content that was wrongly accepted. The W3C XSTS corpus does not exercise this
case, so all four bucket counts are unchanged (vsr=0, isa=67, vir=0, ivi=31); the
gap was surfaced by the libxml2 differential and is covered by a unit test.
2026-06-18 17:23:11 +02:00
0167c92790 fix(schema): a list/union substitution-group member derives only from anySimpleType; invalid-schemas-accepted 69 to 67 (#145)
A substitution-group member whose type is a list or union derives only from
xsd:anySimpleType (XSD Part 2), so it may validly affiliate only to a head typed
anySimpleType, to the same type, or as a recorded restriction of the head's type
(cos-equiv-derived-OK). substitutionTypeErrors skipped the derivation check
entirely when either the member or the head type was a list/union, so a
list/union-typed member affiliated to an unrelated head type (addB141: a union
member of a list-typed head) was accepted.

checkSingleElement now rejects a list/union member type that is not a recorded
derivation of a non-ur head type, before the blanket list/union skip. A list/union
member of an anySimpleType (or untyped, hence anyType) head, the same type, and a
restriction of the head's list/union type are all left valid (isDerived covers the
restriction chains via typeDerivesOrEqual).

invalid-schemas-accepted 69 to 67, no false positive (valid-schemas-rejected and
valid-instances-rejected held at 0). An adversarial review verified untyped and
anySimpleType heads, multi-level restriction-of-list/union members, same-type and
cross-namespace affiliations all still compile.
2026-06-18 16:30:33 +02:00
c3393bcaad fix(schema): key a local element's block by its effective instance namespace; invalid-instances-accepted 32 to 31 (#147)
The namespaced derivation backbone keyed every element by the container 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 was keyed {targetNS}name while the instance element is {}name, so
blockedSubstitutionError's lookup missed and a blocked xsi:type substitution on it
was accepted (typeDef00801: a local Element with block="extension" did not bar an
extension substitution).

gatherElement now keys a local element by localElementNamespace (target namespace
when qualified, none otherwise); global elements stay keyed by the target
namespace, and the bare-keyed tables are unchanged.

invalid-instances-accepted 32 to 31, no false positive (valid-instances-rejected
and valid-schemas-rejected held at 0). An adversarial review confirmed qualified
local elements are unchanged, global and same-named local element keys stay
distinct, and a non-blocked substitution on a local element is still accepted.
2026-06-18 16:12:08 +02:00
7e765a3bc1 fix(schema): reject an xsi:type that names an abstract type; invalid-instances-accepted 34 to 32 (#147)
An abstract type cannot be the type of an instance element (cvc-elt.4.3.2), so an
instance xsi:type naming an abstract type is invalid as a substitution even when
the element's declared type would otherwise admit it (typeDef01101, xsi:type on an
anyType-declared element). The validator only checked for an abstract DECLARED
type with no xsi:type; abstractXsiTypeError now also rejects an abstract xsi:type
substitute (matched against the namespaced abstract-type set). A concrete type is
unaffected.

invalid-instances-accepted 34 to 32, no false positive (valid-instances-rejected
and valid-schemas-rejected held at 0; a valid instance never names an abstract
type via xsi:type), no other bucket moved.
2026-06-18 15:58:08 +02:00
f432254f5c fix(schema): a fixed mixed element must have no element children; invalid-instances-accepted 37 to 34 (#147)
An element with a fixed value constraint carries its fixed character value and no
element children, even under a mixed content type (cvc-elt.5.2.2.2.1). The fixed
check compared only the concatenated text, so a mixed fixed element whose text
matched the fixed value but which also contained child elements (ctZ013c/d) was
accepted. elementFixedErrors now rejects a fixed element that has element
children. An empty element (which takes the fixed value) and text equal to the
fixed value remain valid.

invalid-instances-accepted 37 to 34, no false positive (valid-instances-rejected
and valid-schemas-rejected held at 0; a fixed element never validly carries
element children), no other bucket moved.
2026-06-18 15:51:25 +02:00
ad80877d27 fix(schema): resolve a simpleContent base through complex-type chains; invalid-instances-accepted 39 to 37 (#147)
A complex type's simpleContent derivation can name another complex type with
simpleContent as its base. simpleContentType resolved a non-XSD, non-simpleType
base via context.simpleTypes only, so a complex-type base (absent from that map)
fell back to xs:string and left the content unconstrained: text invalid for the
real base type was accepted (baseTD00101, where Test restricts Test2 which extends
xs:int, yet "b" was accepted).

simpleContentBaseType now resolves the base to an XSD built-in, a named simple
type, or, when the base is a complex type with simpleContent, that type's own
resolved simple type by recursing through complexTypeNodes (cycle-guarded by a
visited set); the deriving node's facets are then applied over the resolved base's
facets. So Test resolves to int and non-int text is rejected, while a valid int
and the accumulated facets along the chain are honoured.

invalid-instances-accepted 39 to 37, no false positive (valid-instances-rejected
and valid-schemas-rejected held at 0), no other bucket moved. An adversarial
review exercised facet accumulation, named-simpleType bases in the chain,
multi-level and extension chains, and cycles, and found no over-rejection.
2026-06-18 15:44:54 +02:00
488f58701a fix(schema): exclude use="prohibited" attributes from effective uses at instance time; invalid-instances-accepted 43 to 39 (#147)
use="prohibited" removes an attribute from a complex type's effective {attribute
uses} (XSD 1.0 §3.4.2): the attribute is then undeclared, so its presence is
rejected when no attribute wildcard admits it (attJ), but is still admitted (and
validated per the wildcard's processContents) when one does (attZ002). The use
had been parsed as merely optional, so a prohibited attribute was always allowed.

AttributeUse gains a `prohibited` flag, set for both a local declaration and an
<attribute ref> use site. The declaration is kept in the type's attribute list so
schema-validity checks still see it, while instance validation excludes prohibited
uses: the matching loop and the wildcard's declared-set skip them, and the ID-type
count ignores them. There is no explicit "prohibited attribute present" error, so
a wildcard that admits the attribute name still governs it (an earlier attempt to
reject outright wrongly overrode the wildcard).

invalid-instances-accepted 43 to 39, no false positive (valid-instances-rejected
and valid-schemas-rejected held at 0, invalid-schemas-accepted unchanged at 69).
An adversarial review confirmed prohibited attributes admitted by a wildcard
(every namespace/processContents variation, local and ref), restriction removal,
and the ID-count interaction are all handled without over-rejection.
2026-06-18 15:28:35 +02:00
91894d5759 fix(schema): reject two ID-typed attributes on one element at instance time; invalid-instances-accepted 45 to 43 (#147)
XSD 1.0 §3.4.6 (cvc-complex-type) allows an element at most one attribute whose
type is xs:ID. A complex type that DECLARES two ID-typed attribute uses is already
a compile-time schema error, but two ID-typed attributes can also reach an element
through an xs:anyAttribute wildcard that admits two ID-typed global attributes
(attZ014), which was accepted.

validateAttributes now counts the present attributes whose effective type is
xs:ID, taking the type from the declared use an attribute matches or the global
declaration a matching wildcard admits (so a restriction of xs:ID counts), and
reports an error when more than one is present. xs:IDREF/xs:ENTITY and a single
ID attribute are unaffected, and a valid instance never carries two, so the check
cannot over-reject.

invalid-instances-accepted 45 to 43, no false positive (valid-instances-rejected
and valid-schemas-rejected held at 0), no other bucket moved.
2026-06-18 15:01:04 +02:00
ca56caf11c fix(schema): default/fixed value for identity-constraint fields; invalid-instances-accepted 50 to 45 (#147)
An identity-constraint (unique/key/keyref) field that selects an absent attribute
or an empty element whose declaration carries a default or fixed value now takes
that value as its identity-tuple component, so two members can collide on the
defaulted value (idG011/idG012 default, idF016/idF017 fixed) where the duplicate
was previously missed.

The default/fixed value constraints on identity-field targets are gathered at
compile time (identityFieldConstraints, mirroring identityFieldTypes) 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 (an
empty node-set), never when present with value ""; a "." element field 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.

To keep within the file/type-size and one-type-per-file limits, FieldValue,
XPathQueryCache, and the schema-location hints were extracted to their own files
(behavior-preserving moves).

invalid-instances-accepted 50 to 45, no false positive (valid-instances-rejected
and valid-schemas-rejected held at 0), no other bucket moved. An adversarial
review probed present-empty attributes, element content winning over a default,
fixed values, keyref matching, multi-field keys, and merged-schema defaults, and
found no over-rejection.
2026-06-18 14:47:47 +02:00
75bb89dac7 fix(schema): bar a substitution-group member blocked by the head's type; invalid-instances-accepted 53 to 50 (#147)
A block on a substitution-group head's TYPE (its {prohibited substitutions}), not
only on the head element, bars a member whose type reaches the head's type by the
blocked derivation method. filterSubstitutions consulted only the head element's
own block, so block="restriction" on the head's complex type let a
restriction-derived member substitute.

The substitution-group filter now drops a member when its derivation method to the
head's type is in the union of the head element's {disallowed substitutions} and
the head type's {prohibited substitutions}. The element-level block="substitution"
outright bar stays element-only (a type's prohibited-substitutions never carries
substitution), and a member derived by a method the block does not list still
substitutes.

invalid-instances-accepted 53 to 50, no false positive (valid-instances-rejected
and valid-schemas-rejected held at 0). An adversarial review confirmed members
derived by an allowed method, the positive disallowedSubst instances, transitive
chains, and blockDefault interactions all still substitute. (A separate
cross-document cross-namespace substitution-group rejection was confirmed
pre-existing on HEAD, not introduced here.)
2026-06-18 14:02:14 +02:00
b3a2bcfe6a fix(schema): flag a baseless or ancestor complex xsi:type as not derived; invalid-instances-accepted 60 to 53 (#181, #147)
A complex type that records no base of its own derives only from xsd:anyType, so
it cannot validly substitute (cvc-elt.4.3.2.1) for an element whose declared type
is a more specific complex type. The not-derived check required the substitute to
be present in the derivation backbone, so naming the declared type's own complex
ancestor, or an unrelated baseless complex type, via xsi:type was accepted.

notDerivedSubstitutionError now treats a complex substitute as in scope even when
it is absent from the backbone: its non-derivation from a non-ur declared type is
certain (a baseless complex type derives only from anyType, and any real
extension/restriction chain is recorded in the backbone, where derivationMethods
finds it). An atomic substitute must still be in the backbone, so a built-in
stays silent. The declared guard excludes the ur-types, so a baseless complex
substitute under an anyType declaration is admitted; a genuine extension or
restriction substitute is accepted.

invalid-instances-accepted 60 to 53, no false positive (valid-instances-rejected
and valid-schemas-rejected held at 0), no other bucket moved. An adversarial
review exercised deep, cross-namespace, redefine, and element-ref-path derivation
chains plus anyType-declared baseless substitutes, and found no over-rejection.
2026-06-18 13:51:17 +02:00
31a4ebacfb fix(schema): extend the xsi:type not-derived check to atomic substitutes; invalid-instances-accepted 66 to 60 (#181, #147)
cvc-elt.4.3.2.1 requires an xsi:type to name a type derived from the element's
declared type. The not-derived check covered only complex substitutes, so an
xsi:type naming an atomic type unrelated to the declared atomic type, such as a
sibling restriction or the declared type's own ancestor, was accepted.

notDerivedSubstitutionError now admits atomic simple substitutes too (via
isAtomicOrComplex, which excludes list/union, handled separately). It still fires
only when both the substitute and the declared type are recorded in the
namespaced derivation backbone and derivationMethods finds no path between them.
Built-in types and the ur-types are absent from the backbone, so a built-in
substitute or declared type leaves the check silent: a valid built-in derivation
such as xsi:type="xs:int" on an xs:decimal element is never flagged, which was
the source of an earlier false-positive regression. A genuine user restriction
chain stays inside the backbone the whole way down, so a validly derived
substitute is accepted.

invalid-instances-accepted 66 to 60, no false positive (valid-instances-rejected
and valid-schemas-rejected held at 0), no other bucket moved. An adversarial
review probed built-in derivation in both directions, deep and cross-namespace
restriction chains, and sibling/ancestor substitutes, and found no over-rejection.
2026-06-18 13:37:49 +02:00
c80659d80d fix(schema): resolve the declared type through the element-ref chain for the list/union xsi:type check; invalid-instances-accepted 78 to 66 (#181, #147)
The list/union xsi:type derivation check decided whether the declared type admits
a list or union substitute from a single type-table lookup, so it never reached
the concrete type of an element declared by reference (`<xs:element ref="x">`):
such an element's list/union xsi:type was wrongly accepted.

declaredAdmitsAnySimpleType now resolves the declared type through the full
element-ref / type-reference chain (resolvedDeclaredType, the type-returning
companion to resolvedDeclaredTypeName) to the concrete .simple/.complex before
deciding. A list or union xsi:type on a ref'd element whose declaration resolves
to a more specific atomic type is now rejected; the chain is cycle-bounded and
falls through to suppress (under-reject) when it cannot resolve, so a ur-typed,
list/union-typed, or unresolvable declared type still admits the substitute.

invalid-instances-accepted 78 to 66, no false positive (valid-instances-rejected
and valid-schemas-rejected held at 0), no other bucket moved. An adversarial
review exercised ref chains to anySimpleType/anyType/untyped declarations, named
and aliased list/union declarations, a deep alias chain, cross-namespace imported
refs, substitution-group heads, atomic restriction substitutes, and a self-
referential alias cycle, and found no over-rejection.
2026-06-18 13:24:32 +02:00
05a0aa4305 fix(schema): reject a list/union xsi:type on a non-ur-typed element; invalid-instances-accepted 81 to 78 (#181, #147)
A list or union simple type is derived only from xsd:anySimpleType (XSD Part 2),
so an xsi:type naming a list or union can validly stand in (cvc-elt.4.3.2.1) only
for an element whose declared type is a ur-type (anySimpleType/anyType). Naming
one on an element of a more specific atomic or complex type, such as
xsi:type="myList" on an xs:int element, is not a valid derivation and is now
rejected.

listUnionSubstitutionError flags the case; declaredAdmitsAnySimpleType suppresses
it when the declared type is a ur-type, is itself a list or union (whose inline
identity cannot be matched), or is an unresolvable reference, so the check biases
to under-reject and never over-rejects. A genuinely derived restriction
substitute, a list/union under an anySimpleType declaration, and a restriction of
a list (which keeps list variety) are all left accepted.

This widening became safe once the derivation backbone was keyed by namespaced
identity (#186); the conservative atomic and built-in not-derived cases remain
under-rejected pending the full type-identity relation.

invalid-instances-accepted 81 to 78, no false positive (valid-instances-rejected
and valid-schemas-rejected held at 0), no other bucket moved. An adversarial
review exercised a list-restriction substitute, a union declared type, ur-type
and typeReference-chain declarations, and cross-namespace list types, and found
no over-rejection.
2026-06-18 12:55:46 +02:00
1fe670581f feat(schema): key the instance derivation/substitution backbone by namespaced identity; re-land blockDefault; invalid-instances-accepted 89 to 81 (#186, #147)
The instance-validity derivation and substitution tables were keyed by bare
local name, so two same-local-name types or elements in different namespaces
collided. In multi-document validation this let one schema's block/derivation
facts shadow another's, and it blocked honouring blockDefault (whose re-land
widened that collision into an over-rejection).

Key the instance subsystem by namespaced identity {ns}local instead. The shared
DerivationTables keeps its bare-keyed fields for the schema-validity machinery
(Particle-Valid-Restriction and the consistency rules, which resolve QNames to
bare names and walk the same backbone) and gains parallel ns* fields built in
the same pass, resolving each container's components through its target
namespace and prefix bindings. Only the instance consumers
(blockedSubstitutionError, notDerivedSubstitutionError, abstractTypeError, the
substitution-group closure, and referenceParticle) read the ns* tables, via a
derivationKey(fromReference:) that reduces a type:{ns}local reference, a bare
name, or an element ref to a {ns}local backbone key. A key miss stays permissive
(no constraint, substitution allowed), so the re-keying cannot over-reject.

With the namespaced backbone in place, blockDefault is re-landed FP-free: a
schema's blockDefault supplies the {disallowed substitutions} of every element
and complex type without an explicit block (own block overrides; block="" clears;
intersected with the methods each component admits), so blockDefault="extension"
bars an extension substitution and "#all" bars all, without one namespace's
defaults shadowing another's.

The pure re-keying preserves single-document behaviour exactly (the corpus is
overwhelmingly single-namespace, where {ns}local and bare coincide): all four
XSTS buckets held at vsr=0 / isa=69 / vir=0 / ivi=89 after re-keying alone.
blockDefault then drove invalid-instances-accepted 89 to 81, no false positive
(valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket
moved. An adversarial review exercised same- and cross-namespace substitutions,
no-targetNamespace keys, simple-type restriction, transitive substitution groups,
and blockDefault override/over-block cases, and the prior multi-document
cross-namespace blockDefault over-rejection is now accepted while a same-namespace
blocked substitution is still rejected (precise, not merely permissive).
2026-06-18 12:41:18 +02:00
0d3fbf1b0a fix(schema): intersect a complex type's attribute wildcard sources; invalid-instances-accepted 94 to 89 (#147)
A complex type's effective {attribute wildcard} is 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 admit nothing and an attribute in either is rejected.

attributeWildcard(under:_:visited:) combined the sources with Wildcard.union,
which let through an attribute that only one source allowed. It now uses a new
Wildcard.intersection covering the namespace cases (##any identity, enumerated
set intersection, enumerated-with-##other exclusion via exclude(_:otherThan:)).
A type with a single anyAttribute source is unchanged (intersection with the
nil identity is the lone wildcard), and a complexContent extension's union with
its base wildcard (XSDComplexContent) is untouched. The inexpressible
##other/##other-different-target and mismatched processContents cases resolve to
the less restrictive result, so the change never over-rejects.

XSTS invalid-instances-accepted 94 to 89, no false positive (valid-instances-
rejected and valid-schemas-rejected held at 0 across the whole corpus), no other
bucket moved. An adversarial review exercised nested groups, ##other/##local and
cross-import namespace cases, processContents, extension-vs-restriction, and
single-source invariance, and found no new over-rejection.
2026-06-18 11:21:32 +02:00
cadfb0d871 fix(schema): compare identity-constraint fields across the decimal value space; invalid-instances-accepted 95 to 94 (#147)
An identity constraint (unique/key/keyref) compares its field values in their
value space. Two fields typed by different members of the decimal-derived
numeric family denote the same value when their numbers are equal, so a
decimal "1" and an unsignedByte "1" (supplied here via xsi:type) collide for a
unique. FieldValue equality previously required the two declared base types to
match exactly and reported them unequal when they differed, so the duplicate
went undetected.

Add BuiltinType.isDecimalDerived (decimal plus the integer family, which share
one value space) and compare those values as DecimalValue. The unification is
confined to that family: float and double are distinct primitive spaces and a
number is never equal to a string, so no valid instance is newly rejected.

XSTS invalid-instances-accepted 95 to 94, no false positive (valid-instances-
rejected and valid-schemas-rejected held at 0 across the whole corpus), no
other bucket moved.
2026-06-18 11:01:05 +02:00
94c6950ae3 fix(schema): process xsd:anyType wildcards as lax; invalid-instances-accepted 101 to 95 (#146, #147)
The ur-type xsd:anyType modeled its element and attribute wildcards with
processContents="skip", so a child element or attribute of an untyped element
(or an element typed xs:anyType) was never validated. XSD 1.0 §3.4.7 specifies
those wildcards as "lax": content that has a global declaration is validated
against it, while undeclared content is admitted. This is why an invalid
declared value reached through a wildcard (e.g. a duration-typed attribute with
value "P" matched by <xsd:anyAttribute/>) was accepted.

Set both wildcards to .lax at all three ur-type construction sites and loosen
isUrType to recognise the lax (or skip) shape.

The lax change required making the wildcard lookup of a global element
declaration namespace-exact. A global element is always in the schema target
namespace, so an unqualified instance element must match only a no-namespace
global, which the qualified key element:{}local already covers. The previous
bare-local-name fallback conflated namespaces and would have validated an
unqualified child against a same-named namespaced declaration; it is removed in
the document path and both streaming sites.

XSTS invalid-instances-accepted 101 to 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. An adversarial review
re-ran the full suite and found no new over-rejection.
2026-06-18 10:44:33 +02:00
67451055f5 fix(schema): block="substitution" bars members even for an untyped head; invalid-instances-accepted 107 to 101 (#147)
block="substitution" (or a block set containing it, including #all) on a
substitution-group head forbids every member from substituting, regardless of
the head's or members' types. filterSubstitutions gated the whole filter --
including this type-independent substitution ban -- on the head having a type,
so an abstract untyped head (the particlesDc cluster) skipped filtering and its
members still substituted. The substitution ban now applies first and
type-independently; the type-derivation block (extension/restriction) still
needs the head's type, unchanged.

An adversarial review confirmed no false positive: the head's own appearance is
computed independently of the member list (a concrete blocked head still
validates), and the typed-and-substitution-blocked case is provably equivalent
to the prior per-member rejection. Disclosed pre-existing under-rejection (not
introduced here): blockDefault="substitution" is not honored, only an explicit
block on the head.

XSTS: invalid-instances-accepted 107 to 101 (particlesDc004/005/006/008/009 and
a sibling), valid-instances-rejected and valid-schemas-rejected held at 0.
2026-06-18 10:13:45 +02:00
8e873dc929 fix(schema): an xsi:type must be derived from the declared type (cvc-elt.4.3.2.1); invalid-instances-accepted 133 to 107 (#181)
First instance-bucket reduction of the campaign. An instance xsi:type naming a
complex type must be derived from the element's declared type. notDerivedSubstitutionError
flags a complex substitute on a different branch of the hierarchy; previously
only a derived-but-blocked substitution was caught (blockedSubstitutionError
returns nil when there is no derivation path), so a non-derived xsi:type was
silently accepted (the test006 cluster, 32 instances, and more).

Conservatively scoped after a broader first cut took 20 false positives (now
reverted): it fires only when the substitute resolves to a COMPLEX type and both
substitute and declared names are in the derivation backbone with no path between
them. Simple/list/union substitutes, ur-types, and unresolvable references are
left alone -- under-reject, never over-reject. An adversarial review confirmed
the conservative guards eliminate the prior false positives; the one residual is
the pre-existing bare-local-name keying of the derivation backbone (shared with
blockedSubstitutionError), disclosed in the code, corpus-absent.

The three xsi:type override checks (blocked / not-derived / abstract) are folded
into one xsiTypeOverrideError helper to keep validateChild under the complexity limit.

XSTS: invalid-instances-accepted 133 to 107 (26 caught), valid-instances-rejected
and valid-schemas-rejected both held at 0, no other bucket moved.
2026-06-18 10:07:05 +02:00
0994f5dd2a fix(schema): include and redefine require a schemaLocation; invalid-schemas-accepted 71 to 69 (#145)
xs:include and xs:redefine require a schemaLocation attribute (only xs:import's
is optional, being a resolution hint). An include or redefine declared without
it is now rejected in the structure walk.

A required-attribute check with no name resolution or reference walking; it
cannot over-reject (schemaLocation is mandatory on these per the
schema-for-schemas), so the full critic loop is not needed here -- reserved for
the false-positive-prone resolution rules. XSTS: invalid-schemas-accepted 71 to
69 (schB3 and a sibling), valid-schemas-rejected held at 0, no other bucket moved.
2026-06-18 01:33:00 +02:00
7a033d28e6 fix(schema): a redefined schema's targetNamespace must match the redefiner (src-redefine); invalid-schemas-accepted 74 to 71 (#161)
A schema pulled in by xs:redefine must have the same targetNamespace as the
redefining schema, or no targetNamespace (chameleon) -- the same constraint
xs:include already enforced. includeCompositionFindings now also walks
xs:redefine, selecting the loaded redefined schema document (guarding the
container lookup with localName == "schema" so the redefine wrapper, which
carries no targetNamespace, is not matched) and comparing its target namespace
to the redefiner's effective (chameleon-resolved) namespace.

An adversarial review confirmed no false positive: same-namespace, chameleon
(no targetNamespace on the redefined schema), and both-no-namespace redefines
are never flagged; only a present-and-different targetNamespace fires.

XSTS: invalid-schemas-accepted 74 to 71 (schI2/schI5 and a schN cross-namespace
case), valid-schemas-rejected held at 0, no other bucket moved.
2026-06-18 01:25:34 +02:00
24a9299975 docs(xsts): account for the remaining invalid-schemas-accepted tail (74)
Adds a dated accounting to the XSTS deviation worklist: the labelled-suite
ratchet stands at 0/74/0/133 with false positives held at 0 across the campaign.
Partitions the remaining 74 into fixable-but-intricate debt (redefine
restriction, NameAndTypeOK union membership, wildcard UPA, chameleon/cross-ns
composition, heterogeneous particle edges) and irreducible-by-design or disclosed
items (RecurseAsIfGroup spec ambiguities, the deliberate regex-tolerance), with
the root cause and false-positive risk for each so the burndown is driven by
understanding. Records that the adversarial critic loop caught three genuine
false positives this session that the suite and differential both missed.
2026-06-18 01:19:38 +02:00
0f90b3a4d3 fix(schema): generalize redefine self-reference to attributeGroups + bound the walk; invalid-schemas-accepted 75 to 74 (#161)
Extends the redefine self-reference rule (redefineSelfReferenceErrors, moved to
SchemaRedefineSelfReference.swift) to attribute groups: a group or attributeGroup
inside xs:redefine may reference itself at most once (src-redefine.6.1.1 /
7.2.1), and a group self-reference must occur exactly once. Catches a redefined
attributeGroup with two self-references (attgC044).

Fixes a latent false positive an adversarial review found in the group case: the
search now uses a BOUNDED walk (boundedSelfReferenceNodes) that stops at nested
element/complexType scopes, matching the established cycle-rule boundary. The
prior unbounded walk would miscount a recursive group reference inside a nested
element's content (a data-structure recursion, legal) as a redefinition
self-reference. attributeGroup content has no such nesting, so it is unaffected.

XSTS: invalid-schemas-accepted 75 to 74 (attgC044), valid-schemas-rejected held
at 0, no other bucket moved. The validation description broadened accordingly
(configuration pin updated).
2026-06-18 01:15:16 +02:00
6379e5b25a fix(schema): an identity-constraint xpath may not use an undeclared prefix; invalid-schemas-accepted 77 to 75 (#145)
A selector/field xpath in an identity constraint may name elements/attributes
with namespace-qualified name tests; every prefix used must be a declared
xmlns: prefix in scope (the implicit xml prefix excepted). identityXPathErrors,
after the existing syntax check, now resolves each name-test prefix against the
in-scope bindings (namespaceBindingsInScope, which captures the schema root and
every ancestor) and rejects an undeclared one.

An adversarial review confirmed no false positive: a prefix declared on the
schema root, any intermediate ancestor, or the selector/field element resolves
correctly; xml, the child::/attribute:: axis keywords (colon-free), prefix:*,
and inter-token whitespace are all handled.

XSTS: invalid-schemas-accepted 77 to 75 (idI010, idJ011), valid-schemas-rejected
held at 0, no other bucket moved. (Two existing xpath-syntax fixtures used an
undeclared 'ns' prefix to test name-test syntax; the test schema now declares
xmlns:ns so they exercise syntax with a bound prefix.)
2026-06-18 01:01:12 +02:00
880137a955 fix(schema): a redefined group's self-reference must occur exactly once (src-redefine.6.1.2); invalid-schemas-accepted 79 to 77 (#161)
A group inside xs:redefine may reference itself (the group being redefined) at
most once, and that self-reference must have minOccurs = maxOccurs = 1. A
self-reference with minOccurs="0", maxOccurs="unbounded", or a count above 1 is
invalid. redefineGroupSelfReferenceErrors flags it, surfaced by the pre-compile
Validation redefineGroupSelfReferencesAreUnit.

The self-reference is matched by NAMESPACE (referenceNamespace == the redefining
schema's targetNamespace), not local name alone, so a redefined group that
references an imported group merely sharing its local name is correctly left
unconstrained -- the false-positive boundary an adversarial review flagged
before merge, mirroring the namespace guard the sibling redefine/cycle rules use.

XSTS: invalid-schemas-accepted 79 to 77 (schR3, schR4), valid-schemas-rejected
held at 0, no other bucket moved.
2026-06-18 00:52:40 +02:00
c45e1eb856 fix(schema): a local element must have a name or a ref; invalid-schemas-accepted 80 to 79 (#145)
A local element declaration (one not a child of schema) must identify itself
with a name or a ref; an xs:element with neither is invalid. localElementErrors
now flags it, alongside the existing local-element forbidden-attribute checks.

XSTS: invalid-schemas-accepted 80 to 79 (elemP005), valid-schemas-rejected held
at 0, no other bucket moved.
2026-06-18 00:34:50 +02:00
75dd044d93 fix(schema): a local complexType must be anonymous; invalid-schemas-accepted 81 to 80 (#145)
A complexType nested in an element is a local (anonymous) type definition and
may not carry a name attribute; only a top-level complexType (a child of schema,
or a redefine redefinition) is named. localComplexTypeNameErrors flags a named
complexType whose parent is an element, wired into the structure-walk validation.

XSTS: invalid-schemas-accepted 81 to 80 (ctA042), valid-schemas-rejected held
at 0, no other bucket moved.
2026-06-18 00:17:35 +02:00
059763f5e9 refactor(schema): surface facet/pattern validity as a Validation, not a parse-time report (#161)
The convergence audit for "validators everywhere" found the one remaining XSD
schema-validity verdict reported outside the Validator framework: facet
definition validity and pattern syntax validity were reported straight to the
diagnostics collector during simple-type compilation (validation folded into
parsing, which validation-rules.md forbids).

These checks genuinely need the resolved base type, computed during compilation,
so the finding is gathered into a context finding sink (XSDContext
.facetFindingSink) during type resolution and surfaced by the named post-compile
Validation simpleTypeFacetsAreValid -- located findings preserved, now pinned in
the configuration test and removable by identity like every other rule.

Re-sweep confirms the XSD path has converged: the only remaining throws are
notASchema (a not-a-schema precondition) and inconsistent (the framework's own
surfacing throw); no validity verdict bypasses the framework. No bucket change
(invalid-schemas-accepted 81, valid-schemas-rejected 0); full suite + fuzz pass.
2026-06-18 00:02:20 +02:00
4def6a2c07 refactor(schema): redefine self-derivation as a Validation; drop the redundant all-group throw (#161)
Continues incorporating every schema-validity check into the Validator framework
(Rules/universal/validation-rules.md) rather than ad-hoc parse-pipeline throws.

- checkRedefine (src-redefine.5: a redefined type restricts or extends itself)
  becomes the pre-compile Validation redefinitionsDeriveFromThemselves;
  redefineDerivationErrors returns findings instead of throwing.

- checkAllGroups is removed entirely: every check it made is already produced by
  Validations -- "a member must be an element" by schemaStructureValid
  (allowedChildren[all]), and the occurrence limits by allGroupLimitedErrors
  (cos-all-limited.2). Verified redundant and not load-bearing: an empirical
  probe with the calls removed left invalid-schemas-accepted at 81 unchanged,
  the fuzz suite's "compile and validation never crash or hang" still passes
  (the NFA builder's own guards hold), and false positives stayed 0.

compileRule is now internal so composition Validations can live in their own
file (SchemaCompositionValidations.swift). Both new pre-compile rules are pinned
in ValidationConfigurationTests. The pre-existing public SchemaError.invalidAllGroup
case is kept (released API) though no longer thrown. No bucket change
(invalid-schemas-accepted 81, valid-schemas-rejected 0); full suite + fuzz pass.
2026-06-17 23:49:30 +02:00
d3a0b60d67 refactor(schema): express the src-resolve check as a Validation, not a pipeline throw (#161)
Per the OpenAPIKit validation idiom (Rules/universal/validation-rules.md), every
schema-validity check should run through the Validator framework as a described,
composable Validation rather than an ad-hoc throw. The src-resolve check (a
resolved include/import/redefine schemaLocation must be a well-formed schema)
was a parse-pipeline throw; it is now the pre-compile Validation
referencedSchemasResolveToSchemas.

failedSchemaReferences (formerly the throwing checkSchemaLocations) returns the
resolved-but-not-a-schema locations; the pipeline records them on XSDContext and
the validation reports one finding each. Same rejection behaviour (the finding
makes the schema invalid), now located in the framework, pinned in the
configuration test. Removed the now-unused SchemaError.invalidSchemaReference
case (added earlier this branch, never released).

No bucket change (invalid-schemas-accepted stays 81, valid-schemas-rejected 0);
full suite and fuzz pass (report-and-continue does not crash downstream).
2026-06-17 23:34:33 +02:00
a444604a80 fix(schema): a redefined type must derive from itself, not a same-named foreign type (src-redefine.5); invalid-schemas-accepted 84 to 81 (#161)
A type inside xs:redefine must restrict or extend the type it redefines, which
lives in the redefining schema's own target namespace. checkRedefine compared
local names only (derivationInfo strips the prefix), so a base like foo:b-ct
-- an imported type sharing the redefined type's local name -- was treated as
self and accepted. Now the base's namespace is resolved: a base bound to an
explicit namespace other than the redefining schema's target namespace is not
self and is rejected (src-redefine.5).

redefineBaseIsForeign (SchemaImportNamespace.swift) leaves a base with no
resolvable namespace alone, so an unprefixed or own-namespace self-reference
never over-rejects -- the false-positive boundary, the same namespace-aware
fix applied across the campaign.

XSTS: invalid-schemas-accepted 84 to 81 (schQ2/schQ4/schP3),
valid-schemas-rejected held at 0, no other bucket moved. Differential:
agree-valid unchanged at 3161, PureXML-stricter unchanged at 87 over 9618
schemas. Third step of the cross-document composition campaign (#161).
2026-06-17 23:00:07 +02:00
c418147d30 fix(schema): an import namespace must match the imported targetNamespace (src-import.3); invalid-schemas-accepted 87 to 84 (#161)
src-import.3.1/3.2: an xs:import's namespace attribute must equal the
targetNamespace of the schema document it resolves to (both present and equal,
or both absent). Importing a no-namespace document under a declared namespace,
or one whose target namespace differs, is now rejected.

importNamespaceErrors (new SchemaImportNamespace.swift) builds a resolved
schemaLocation -> targetNamespace map from the loaded containers and compares
each import node's namespace to it. Checked only when the schemaLocation
resolved to a loaded schema (an unresolved import is the processor's choice,
not an error) -- the false-positive boundary; imports keep their own target
namespace so no chameleon adjustment applies.

XSTS: invalid-schemas-accepted 87 to 84 (schF6/schG10/schG13),
valid-schemas-rejected held at 0, no other bucket moved. Differential:
agree-valid unchanged at 3161, PureXML-stricter unchanged at 87 over 9618
schemas. Second step of the cross-document composition campaign (#161).
2026-06-17 22:47:24 +02:00
7f2943cf5c fix(schema): a resolved schemaLocation must be a valid schema (src-resolve); invalid-schemas-accepted 95 to 87 (#161)
When an xs:include / xs:import / xs:redefine schemaLocation IS resolved (the
schema loader returns content for it), that content must be a well-formed
schema document. Not-well-formed XML, or well-formed XML whose root is not
xs:schema, now makes the composing schema invalid (SchemaError
.invalidSchemaReference). collectContainers previously loaded externals with
`try? parseTree` and silently dropped a malformed one, accepting the schema.

A location the loader does NOT resolve (returns nil) is not an error, since
resolution is the processor's choice (src-resolve) -- so a self-contained
schema, or one whose externals are simply unavailable, is unaffected. This is
the false-positive boundary: fire only when the loader returned content that
is not a schema.

XSTS: invalid-schemas-accepted 95 to 87 (eight caught), valid-schemas-rejected
held at 0, no other bucket moved. Differential: agree-valid unchanged at 3161,
PureXML-stricter unchanged at 87 over 9618 schemas (zero new over-rejection).
First step of the cross-document composition campaign (#161).
2026-06-17 22:34:21 +02:00
615f668fc2 fix(schema): an attribute's type must be a simple type (au-props-correct); invalid-schemas-accepted 96 to 95 (#145)
The {type definition} of an attribute is 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 now 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, consistent with the
src-ct rules.

Strictly zero false positive: an attribute can never carry a complex type, so
no valid schema is affected. XSTS: invalid-schemas-accepted 96 to 95 (attD002),
valid-schemas-rejected held at 0, no other bucket moved. Differential:
PureXML-stricter unchanged at 87 over 9618 schemas.
2026-06-17 22:12:01 +02:00
a2047956cb fix(schema): enforce element value-constraint content type (cos-valid-default); invalid-schemas-accepted 99 to 96 (#145)
An element with a default or fixed value constraint must have a content type
that admits character data: a simple type, or mixed content (cos-valid-default
/ e-props-correct). An element whose type is, or whose inline complex type has,
element-only or empty content may not carry a value constraint.

elementValueConstraintContentErrors flags only a content type that is certainly
element-only or empty; a simple type, mixed content, xs:anyType (mixed), an
untyped element (anyType), a complexContent-derived type (mixedness may be
inherited), and a foreign or unresolved type are all left alone, so it cannot
over-reject.

XSTS: invalid-schemas-accepted 99 to 96 (elemG003/elemG004/elemD004),
valid-schemas-rejected held at 0, no other bucket moved. Differential:
PureXML-stricter unchanged at 87 over 9618 schemas.
2026-06-17 22:00:23 +02:00
9c57dedb64 fix(schema): enforce derivation base-type kind (src-ct.1/.2); invalid-schemas-accepted 104 to 99 (#145)
A content-model derivation's base must match its content-model keyword
(XSD 1.0 Structures 3.4.6):

- src-ct.1: a complexContent restriction/extension base must be a complex
  type. A simpleType or a built-in simple type (xs:string) is rejected; the
  ur-type xs:anyType is complex and remains a valid base.

- src-ct.2 (extension): a simpleContent extension base must be a simple type
  or a complex type with simple content. 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, e.g. xs:openAttrs) is not rejected. This caught a
self-inflicted false positive during development; the fix restores it.

XSTS: invalid-schemas-accepted 104 to 99 (ctE003/ctE004/ctK002/ctJ002/
ctJ003), valid-schemas-rejected held at 0, no other bucket moved. The
differential corroborates: PureXML-stricter unchanged at 87 over 9618
schemas (zero new over-rejection).
2026-06-17 21:49:12 +02:00
606bc35a45 fix(schema): enforce identity-constraint content model; invalid-schemas-accepted 110 to 104 (#145)
An xs:unique / xs:key / xs:keyref content model is (annotation?, selector,
field+): exactly one selector, then one or more fields, in that order. The
structure walk previously checked only that the children were drawn from
{annotation, selector, field}, not their cardinality or order, so a field
before the selector, a second selector, or a missing field was accepted.

identityConstraintContentErrors enforces it, wired into the per-component
structural dispatch. XSTS: invalid-schemas-accepted 110 to 104 (six caught:
idA044/046, idB044/046, idC047/049), valid-schemas-rejected held at 0, no
other bucket moved.
2026-06-17 21:04:16 +02:00
b391db6a74 fix(schema): enforce model-group occurrence ranges; invalid-schemas-accepted 130 to 110 (#145)
Enforce the two occurrence-range constraints on model groups that PureXML
was not checking:

- p-props-correct.1: a particle's minOccurs may not exceed its maxOccurs,
  both defaulting to 1 when the attribute is absent. occurrenceOrderErrors
  previously fired only when BOTH attributes were present, so it missed
  minOccurs="2" with a defaulted maxOccurs (2 > 1) and maxOccurs="0" with a
  defaulted minOccurs (1 > 0). It now orders against the effective defaults.

- cos-all-limited.2: an all group's maxOccurs must be 1 and its minOccurs
  0 or 1, and every particle it contains is limited to maxOccurs 0 or 1.

Both are zero-false-positive rules (no valid schema violates them). XSTS:
invalid-schemas-accepted 130 to 110 (20 caught), valid-schemas-rejected
held at 0, no other bucket moved. The differential harness corroborates:
agree-invalid 1751 to 1767, PureXML-stricter unchanged at 87 (no new
over-rejection across 9618 schemas).
2026-06-17 20:55:22 +02:00
bbd616b3a4 test(diff): add opt-in differential schema-validity harness vs libxml2 (#145)
Production-readiness stopper #3: characterise correctness against an independent
reference. For each .xsd under XSTS_ROOT, compares PureXML's compile verdict with
libxml2 (xmllint --schema) and writes disagreements to /tmp/xsts-differential.txt.

Over 9618 schemas: agree on 4912 (3161 valid, 1751 invalid). The 87 where PureXML
is stricter are triaged as PureXML being more correct (libxml2 omits facet
consistency, EDC, whiteSpace restriction, au-props, notation), independent
corroboration that PureXML does not over-reject. The 117 where libxml2 is stricter
are a false-negative triage queue (stopper #2). Opt-in; xmllint and the corpus are
test-harness-only, not package dependencies.
2026-06-17 20:15:17 +02:00
d92b9149df fix(schema): eliminate the last false positive (particlesZ001); false-positives = 0 (#148)
Read element-vs-choice Particle Valid (Restriction) through effective total
range: an element validly restricts a base 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 fixes particlesZ001 (the
RecurseAsIfGroup rule its own test calls ambiguous; W3C reads it valid) while
keeping particlesM002/003 valid and particlesL rejected.

Named, bounded under-rejection (exact in-branch sequencing unmodelled), the
production-sanctioned direction: invalid-schemas-accepted 128->130,
invalid-instances-accepted 132->133 (particlesZ001.i, a masked gap now exposed).

XSTS valid-schemas-rejected 1->0. With valid-instances-rejected already 0,
false positives are now 0 in both buckets (production stopper #1).
2026-06-17 19:56:57 +02:00
a23757e958 test(xsts): exclude the \d=\p{Nd} spec-divergent instances; valid-instances-rejected 0 (#146)
reS17/reS38/reZ004v assert \d matches Ethiopic digits (category No), contrary to
the normative \d = \p{Nd} (XSD Datatypes Appendix F). Verified our \d matches Nd
(incl. Khmer U+17E0) and rejects Ethiopic No, as conformant processors do.
Excluded as a named, bounded spec divergence rather than corrupting \d.

XSTS valid-instances-rejected 3 to 0; the false-positive metric now reflects
spec correctness on the instance side (one schema, particlesZ001, remains).
2026-06-17 19:34:49 +02:00