Commit Graph

426 Commits

Author SHA1 Message Date
fa3552876a fix(schema): saturate effectively-unbounded content-model counters (#192, partial)
The counted content-model automaton represents its active set as
(state, counter-vector) configurations bounded by states times the product of
the per-counter saturation caps. An unbounded counter already saturates at its
minimum (every higher count is matching-equivalent). A finite maximum the input
cannot reach is equivalent: the active set increments a counter at most to its
cap, the cap is at most inputLength+1, so when maximum > inputLength+1 the value
can never reach maximum, belowMaximum is always satisfied, and every value at or
above the minimum is indistinguishable. Saturate such a counter at its minimum
too, so a huge but finite maxOccurs on a nested counter no longer multiplies the
active set by an inputLength factor per nesting level.

A document that did not terminate within a minute against the Z035 worst case
(nested sequences/choices with maxOccurs up to 1e11) now validates in linear
time. No accept/reject decision changes: the full XSTS suite holds at
0/35/0/26, and an adversarial no-caps reference-matcher differential found no
false positive or false negative, including the nullable-body case (where the
builder makes canExit unconditional, so the counter value is matching-irrelevant)
and the occurrence boundary. Guarded by ContentMatcherWorstCaseTests.

Partial: a model with several moderate (input-reachable) maxOccurs nested deeply
still enumerates a large active set; the general polynomial bound (a position-
indexed DP over the counting automaton) remains under #192.
2026-06-21 13:16:24 +02:00
ca7a7d1433 fix(schema): a reference to an all group must have maxOccurs 1
XSD 1.0 cos-all-limited: a reference to a model group whose content is an
`all` group is itself an all-group particle, so its maxOccurs must be 1 and
its minOccurs 0 or 1. The direct `<all>` element was already checked by
allGroupLimitedErrors, but a `<group ref>` resolving to an all group was not,
so corpus particlesEa025 (`<group ref="x:G" maxOccurs="2">` to an all group)
was accepted.

Add allGroupReferenceMaxOccursErrors: collect the names of group definitions
whose content is an all group, then flag a group reference to one whose
maxOccurs is not 1 or whose minOccurs is not 0/1. The check runs over a single
schema document tree, where group names are unique, so the by-local-name match
cannot collide; a cross-document reference lives in a separate container and is
under-rejected (never falsely rejected).

XSTS invalid-schemas-accepted 36 -> 35; valid-schemas-rejected held at 0.
2026-06-20 19:14:33 +02:00
ee676cb470 fix(schema): restriction may not narrow an element to an extension-derived type
XSD 1.0 cos-particle-restrict NameAndTypeOK.2.2: when a complexContent
restriction narrows an element, the element's type must be validly derived
from the base element's type EXCLUDING extension (and list/union), i.e. by
restriction only. The element-type check followed the derivation chain
regardless of method, so a type derived from the base by extension was wrongly
accepted as a valid restriction (corpus particlesIj008).

Add typeDerivesByRestriction (as typeDerivesOrEqual, but the user-declared
chain is followed across restriction steps only; an extension step breaks the
walk) and use it on the non-union fall-through of elementTypeRestrictionOK.
The shared typeDerivesOrEqual is untouched, so substitution-group validity
(where extension is legal) is unaffected. Built-in lattice and union-base
derivation are unchanged, so valid restrictions, including the union-member
restriction addB150, still compile.

XSTS invalid-schemas-accepted 37 -> 36; valid-schemas-rejected held at 0
across all 14383 groups. Union-base (Ik011/Ik025/Ik027) and substitution-group
(V020) cases need the by-name union-member and ref-resolution clauses and stay
deferred.
2026-06-20 18:34:50 +02:00
f6159191db fix(schema): compare simpleContent fixed value in value space
A fixed value on an element whose type is a complex type with simpleContent
was compared as a raw string, so value-space-equal content was wrongly
rejected (e.g. "05" against an xs:int fixed "5"). The complex-element fixed
check on both the tree path (validate(_:against:)) and the streaming path
passed no value type to elementFixedErrors, falling through to a string
compare; both now pass the simpleContent simple type as the value type, in
lockstep, so the comparison is in value space (cvc-elt.5.2.2.1) as the
simple-typed-element path already does.

XSTS gate held at 0/37/0/26 across all 14383 groups.
2026-06-20 18:08:32 +02:00
aeff7869f9 fix(schema): streaming validator applies default/fixed value constraints
The streaming XSD validator ignored an element's default/fixed value constraint
in simple-content validation, so an empty element with a value constraint was
validated against the empty string (a false positive), and a present fixed
value was compared as a raw string rather than in the type's value space.

Mirror the tree path's validateChild for the simple-typed element:

- an empty element takes its constraint value via a shared effectiveSimpleText
  helper both paths use; the constraint comes from the matched particle (keyed
  by namespaced identity), not the flat by-local-name elementConstraints map,
  so same-local-name elements in different types do not collide.
- a present fixed value is compared in the type's value space and against the
  matched particle's fixed value (elementFixedErrors with valueType and
  particleFixed), not a raw string compare ("05" equals int fixed "5").

The particle constraint is looked up from the parent's content model at
element-close time. Streaming now agrees with the tree-path oracle on value
constraints, verified by an adversarial differential (13 configurations).
No XSTS bucket changes; gate held at 0/37/0/26.
2026-06-20 17:38:45 +02:00
f9b56077cc fix(schema): streaming xsi:type and type resolution match the tree path
The streaming XSD validator resolved element and xsi:type references by bare
local name, while its type tables are keyed by namespaced identity
({ns}local). A prefixed xsi:type in an imported namespace missed (a false
"unknown xsi:type") or hit a same-local-name type in the wrong namespace, and
two global elements sharing a local name across namespaces collided.

Resolve through in-scope prefix bindings accumulated down the element stack,
keyed by namespaced identity, mirroring the tree path:

- the xsi:type override type, the undeclared-type check, and root/child
  element declarations are namespaced; root resolution mirrors the tree's
  globalElementDeclaration (bare local-name map only for a no-namespace root).
- run the full cvc-elt.4 substitution gate the tree path runs (blocked /
  abstract / not-validly-derived / list-or-union / anyType-for-anySimpleType
  override, and an abstract declared type carrying no xsi:type).
- resolve a type from an element's own xsi:type where there is no governing
  declaration: an undeclared root (the Sun target-namespace fallback) and a
  strict-wildcard child, both via a shared xsiTypeNamedType helper.

Streaming now agrees with the tree-path oracle across these, verified by an
adversarial streaming-vs-tree differential. No XSTS bucket changes (the tree
path is the conformance oracle); gate held at 0/37/0/26.
2026-06-20 16:38:56 +02:00
d8794579db fix(schema): forbid attribute declarations in the XSI namespace
XSD 1.0 Schema Component Constraint "xsi: Not Allowed" (§3.2.6): an attribute
declaration's {target namespace} must not be the XSI namespace. A schema may
target XSI and declare unqualified attributes (they land in no namespace),
but a top-level attribute, or a local one made qualified by form="qualified"
or attributeFormDefault="qualified", lands in XSI and is forbidden.

Add xsiNamespaceAttributeErrors, scoped to schemas whose target namespace is
XSI, registered as the pre-compile rule attributeDeclarationsNotInXSI. It
rejects corpus attKb018a (qualified attribute-group attributes) and attKa015
(top-level attributes), while attKb018 and attKc018 (local unqualified) stay
valid.

XSTS invalid-schemas-accepted 39 -> 37; valid-schemas-rejected held at 0; no
other bucket moved.
2026-06-20 14:33:14 +02:00
7256df1ceb fix(schema): reject references into never-imported namespaces
On the cross-document skip path (a schema declares import/include/redefine
but no external document was loaded), the reference check stood down whole,
so a prefixed reference whose namespace is never imported was accepted even
though it can resolve to no component under any loading (corpus schZ011's
d:d, where namespaces a/b/c are imported but d is not).

Add undeclaredNamespaceReferenceErrors, run only on that skip path, to flag
a reference whose namespace is neither the target, the XSD or XML namespace,
nor named by any xs:import. To stay sound when the import closure is unknown,
it stands down entirely the moment any external carries a schemaLocation:
that document may transitively import the referenced namespace (corpus
schZ004 imports only a, yet a's document imports b, so b:b resolves). It
applies only when every external is location-less and the closure is
provably complete.

XSTS invalid-schemas-accepted 43 -> 39 (schZ011_a-d); valid-schemas-rejected
held at 0; no other bucket moved.
2026-06-20 13:50:55 +02:00
2a9a5afaa9 fix(schema): a non-nillable element must carry no xsi:nil at all (cvc-elt.3.1)
cvc-elt.3.1: when an element's declaration is not nillable, the xsi:nil attribute
is forbidden entirely, whatever its value. PureXML only rejected xsi:nil="true" on
a non-nillable element, so xsi:nil="false" slipped through. ComplexValidator.nilErrors
now flags the presence of any xsi:nil on a non-nillable element via the new
nilAttributeValue helper, while a nillable element still accepts xsi:nil="false"
(explicitly not nilled, valid empty-or-content per its type).

XSTS invalid-instances-accepted 28 -> 26 (both xsi:nil-on-non-nillable cases), no
other bucket moved (valid-schemas-rejected and valid-instances-rejected held at 0
across all 14383 groups). 1452 unit tests pass.
2026-06-20 03:22:05 +02:00
45f2e7dbf5 fix(schema): an untyped substitutionGroup member inherits its head's type
XSD 1.0 3.3.2: an element with neither a `type` attribute nor an inline type, but
a `substitutionGroup`, takes the {type definition} of its head. PureXML defaulted
such an element to `anyType`, so an instance of it accepted any content.
`XSDParser.elementType` now resolves the head declaration through
`context.globalElements` (transitively, with a cycle guard) and uses its type.

XSTS invalid-instances-accepted 29 -> 28 (typeDef00204m: <root>Yes</root> is no
longer accepted where root substitutes a boolean head), no other bucket moved
(valid-schemas-rejected and valid-instances-rejected held at 0 across all 14383
groups). 1451 unit tests pass.
2026-06-20 02:47:07 +02:00
6927eb26f1 fix(schema): reject xsi:type=anyType on an anySimpleType-declared element
cvc-elt.4.3.2.1 requires an xsi:type to name a type DERIVED from the element's
declared type. anyType is the ur-supertype of every type, so it is never derived
from anySimpleType (its own subtype); an anyType override on an anySimpleType
element is therefore invalid. The general not-derived check stays silent on
ur-type declared types to avoid false positives, so this adds the narrow,
provably-invalid anyType-for-anySimpleType pairing only, leaving anyType valid on
an anyType-declared element (which resolves to a complex ur-type and is excluded).

XSTS invalid-instances-accepted 30 -> 29 (stZ056), no other bucket moved
(valid-schemas-rejected and valid-instances-rejected held at 0 across all 14383
groups). 1450 unit tests pass.
2026-06-20 02:09:51 +02:00
01650a454e fix(schema): reject year 0000 in dateTime, date, gYearMonth, gYear
XSD 1.0 has no year zero (the year before 0001 is -0001), so 0000-01-01T00:00:00
and the other 0000 forms are not valid values. DateTimeParser.parseYearOnly now
rejects a zero magnitude for both signs, so negative years like -0001 still parse.

Closes the XSTS dateTime011 false negative: invalid-instances-accepted 31 -> 30,
no other bucket moved (valid-schemas-rejected and valid-instances-rejected held at
0). 1449 unit tests pass; the full XSTS suite settles at 0/43/0/30.
2026-06-20 01:31:14 +02:00
cd6201ca5c fix(schema): make the container-location root fallback live
containerLocationMap stored `loc ?? rootLocation` into a [ObjectIdentifier:
String?]. Because the value type is itself optional the coalescing bound at the
outer optional and rootLocation was never used (the compiler flagged it), so a
container with no location of its own (the compile root) mapped to nil even when
a circular import had loaded the root under a schemaLocation. An explicit String?
annotation keeps the coalescing at the String level so the root inherits
rootLocation.

Conformance is unchanged: 1448 unit tests pass and the full XSTS suite still
settles at 0/43/0/31. The SchemaNameUniqueness root-index dedup case is kept, not
removed: an experiment removing it on the theory it was now redundant regressed
three valid schemas to rejected, so it does real work beyond the fixed fallback.
2026-06-19 23:32:04 +02:00
73ba9b1f38 fix(schema): bound content-matcher configuration set per counter
The counted content automaton clamped every occurrence counter to a single
inputLength + 1 ceiling, so a content model with nested occurrence bounds let
the epsilon-closure grow toward inputLength raised to the counter count. The
XSTS particlesQ005 instance (about 1000 elements against
sequence{1,1000}(e1{1,unbounded})) drove that to roughly 16M configurations per
closure and hundreds of millions of hash operations, so validation did not
terminate in reasonable time.

Each counter now saturates at min(maximum, inputLength + 1), and an unbounded
counter at its minimum: once the minimum is reached every higher count is
matching-equivalent, because canExit only tests the minimum and belowMaximum is
vacuous, so collapsing them is exact. The active-configuration set is then
bounded by the content model rather than by the input length.

particlesQ005 goes from a multi-minute near-hang to about two seconds with
identical results. The 1448 unit tests pass and the full XSTS suite still
settles at 0/43/0/31, and now completes instead of hanging.
2026-06-19 23:32:04 +02:00
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
b73384bc2e fix(schema): count content-model occurrences 2026-06-19 23:32:04 +02:00
f8fd865456 fix(schema): carry global ref metadata into restrictions 2026-06-19 09:03:49 +02:00
55713bbbc0 fix(schema): stop blocked substitution chaining 2026-06-19 08:37:19 +02:00
f04ab8afa1 Reject complex substitutions for anySimpleType heads 2026-06-18 23:04:11 +02:00
00995071b0 Reject mixedness-changing complexContent extensions 2026-06-18 22:37:49 +02:00
ed38a76acd Reject invalid simpleContent inline restrictions 2026-06-18 22:01:01 +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
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