Commit Graph

42 Commits

Author SHA1 Message Date
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
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
ed38a76acd Reject invalid simpleContent inline restrictions 2026-06-18 22:01:01 +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
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
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
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
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
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
ee5d180ba3 fix(schema): enforce the NameAndTypeOK block-superset rule (#145)
Particle Valid (Restriction) now requires a restricting element's {disallowed
substitutions} to be a superset of the base element's: a restriction may block
more substitutions, never fewer. The element particle (Term.element) carries a
block set, populated from the element's block attribute or the schema's
blockDefault.

XSTS invalid-schemas-accepted 179 to 163, no other bucket moved.
2026-06-17 16:45:35 +02:00
4df9eda52e feat(schema): compile import-chained documents as a union (#161)
Schema documents that form an import chain are now compiled as a single
union (Document.init(composing:)) rather than as separately compiled
documents merged after the fact. Merging froze each document's content
models with only the substitution-group members visible in its own import
closure, so a substitution member declared in an importing document never
reached the head's content model in the imported document.

This clears XSTS ctZ007, a cross-document substitution group, taking
valid-instances-rejected (#146) from 4 to 3 with no other bucket moving
(now 1/262/3/147). The union pools each document's container closure into
one context, deduplicating a document supplied directly and also reached
through another's import by target namespace and component-name set.

The XSTS runner uses the union only for import chains, detected by matching
import/include/redefine schemaLocation basenames against the listed
documents; independent roots stay on the lenient per-document merge, which a
union would wrongly trip with cross-document checks.

The remaining three valid-instance rejections are the Ethiopic-digit \d
cases (reS17, reS38, reZ004v), which contradict \d = \p{Nd}.
2026-06-16 23:16:26 +02:00
be8a39394d fix(schema): advance XSTS valid-instances-rejected to 4 (#146)
Consolidates the in-progress #146 work; valid-instances-rejected reaches 4
on the 2006-11-06 XSTS archive (buckets now 1/262/4/147).

Regex: match Unicode scalars rather than grapheme clusters; the empty
pattern matches only the empty string; pin the category repertoire to
Unicode 3.2, the version the corpus is consistent at, so code points added
in 3.2 (Tagalog, Cyrillic Komi) are word characters while later additions
(the Tamil digit zero) stay unassigned and match \D. New XSDCategory and a
3.2 assignment table derived from DerivedAge, replacing the old Unicode31
files.

Content model: assess each child against the particle the content automaton
matched, not a by-name lookup (#180). Two same-named particles keep their
own fixed values, and an element matching a skip wildcard is no longer
validated against its global declaration. Selection is made reproducible
when an ambiguous schema slips past the UPA check.

XSTS runner: compile and merge every schemaDocument in a schemaTest, not
just the first; this also surfaced one previously-masked invalid-instance
acceptance (recorded in the baselines).

Also folds in attribute-group anyAttribute inheritance, extension wildcard
union, namespace-qualified type tables for multi-document
xsi:schemaLocation, per-container elementFormDefault, strict and lax
attribute wildcard validation, built-in type resolution, and xs:redefine
simpleType overrides.
2026-06-16 22:22:05 +02:00
489275c6b8 feat(schema): locate compile diagnostics with validation coding paths (#169)
Schema compile findings now carry ValidationError coding paths that resolve to source spans on a ranged tree, enabling IDE underlines for id, structure, include, and facet violations.
2026-06-16 10:26:11 +02:00
229d7df3d0 feat(validation,schema): composable validation framework and cross-document composition
Ship the OpenAPIKit-style validation layer (builtin rules, warnings channel,
Validator tiers, coverage gates) and wire XSD compile checks through it. Advance
#161 with chameleon include validation, multi-container reference resolution,
and include targetNamespace composition rules so merged schema sets compile
correctly when a schemaLoader is provided.
2026-06-16 09:57:43 +02:00
55daca46be fix(schema): implement facet applicability and validation constraints 2026-06-16 00:02:04 +02:00
434bfc85ea fix(schema): validate duplicate names across separate files with identical content 2026-06-15 22:57:00 +02:00
d2f18a2685 feat(schema): substitution member type must derive from head (e-props-correct.4)
An element declaring substitutionGroup="head" whose own type is unrelated to
the head's type was accepted; XSD 1.0 e-props-correct.4 requires the member's
type be validly derived from the head's. The check runs after named types
resolve and reuses the restriction/extension + built-in-lattice derivation
oracle (an ur-type head admits any member).

It is conservative so it never rejects a valid member: it runs only for a
self-contained schema (no import/include/redefine, where the derivation table
is complete and single-namespace names are unambiguous), reads the member's
type from its own type attribute and the head's from the top-level element
declarations only (so a local element of the same name supplies neither), and
stands down when either type is a list or union (cos-st-derived-ok clauses
2.3/2.4 union-member derivation is not modelled).

The post-namedTypes consistency calls move into a postNamedTypeErrors
aggregator alongside consistencyErrors. XSTS: invalid schemas accepted
480 to 479, no other bucket raised.
2026-06-15 13:08:38 +02:00
4996d75879 fix(schema): an ID-derived attribute or element may not have a value constraint
An attribute or element whose type is or derives from xs:ID may not carry a
default or fixed value (a-props-correct.3 / e-props-correct.5). Such schemas
were accepted.

The check runs after named simple types are resolved, so a named user type
derived from xs:ID is recognized (not just the built-in and inline forms). The
type reference's namespace is resolved from its prefix: only an ID in the XSD
namespace is the built-in, and a user type is ID only when it resolves to one
whose base is xs:ID. An adversarial critic caught a namespace-blind over-
rejection (a user type named ID, or an imported foo:ID, mistaken for the
built-in); the namespace resolution fixes it, with a regression test.

XSTS: invalid schemas accepted 489 to 481 (attZ013a/b, elemZ032a/b,
valueconstraint01001m2/m3/m5/m6 now rejected), no other bucket raised.
2026-06-15 11:41:26 +02:00
067f04184f fix(schema): enforce attribute-use uniqueness and the single-ID rule
A complex type's or attribute group's attribute-use set (flattened through
attributeGroup references) may not contain two distinct declarations with the
same name and namespace (ct-props-correct.4 / ag-props-correct.2), nor more than
one attribute whose type is or derives from xs:ID (ct-props-correct.5).

Declarations are deduplicated by the identity of their declaring node, so the
same declaration reached by two attribute-group paths (a diamond) is one use,
not a clash. Names carry their resolved namespace (form / attributeFormDefault
for locals, the prefix binding for refs), so a local attribute and a reference
to a same-local-name attribute in an imported namespace do not collide. The
check runs after named simple types are resolved, so a named user type derived
from xs:ID is recognized, not just the built-in and inline forms.

XSTS: invalid schemas accepted 505 to 495 (attQ008/009/012/013, attgD006/007/
009/017, ctM003/004 now rejected), no other bucket raised. An adversarial critic
caught a false positive on same-local-name-different-namespace (fixed by
resolving the ref namespace) and a missed named-user-ID type (fixed by ordering
the check after type resolution). Disclosed under-rejections: the set inherited
through extension is not composed in; a duplicate needing an unprefixed
reference with no default namespace is not detected.
2026-06-15 10:29:40 +02:00
96ec9cd1e0 fix(schema): reject circular type derivation (ct-props-correct.3 / st-props-correct.2)
A complex or simple type may not derive, transitively through its base, from
itself. Such schemas were accepted. The check walks the base chain built from
each type's complexContent/simpleContent extension/restriction base (and a
simpleType's restriction base), following an edge only when the base resolves to
the schema's own target namespace.

Three exceptions keep it from over-rejecting: element content that recurses (an
element of the type's own type) is not a derivation and is not followed; a type
redefined in xs:redefine legitimately names itself as its base, so redefined
types end the chain; and the ur-types (anyType/anySimpleType/anyAtomicType,
which the schema-for-schemas bootstraps with self-referential definitions) end
the chain. The target-namespace gate prevents a local type whose base is a
same-local-name type in another namespace from being mistaken for a self-cycle
(found by an adversarial critic; the engine resolves by local name).

XSTS: invalid schemas accepted 519 to 516 (addB101, elemM003, xsd007.e now
rejected), no other bucket raised. Union/list and genuine cross-namespace
derivation cycles are a disclosed under-rejection.
2026-06-15 01:26:37 +02:00
828ed195f0 fix(schema): resolve element-ref namespaces by prefix binding, not target namespace
An `<xs:element ref="p:foo">` had its prefix stripped and the element assumed to
be in the schema's target namespace. That mis-qualified every reference to an
imported element: a restriction of an `##other` wildcard by such a reference was
rejected (the element looked like the target namespace, which `##other` forbids),
and the same mis-qualification rejected valid instances whose elements were
correctly in the imported namespace.

The reference's QName is now resolved by standard XML namespace rules against the
schema root's xmlns bindings: a bound prefix gives its namespace (an imported
element lands in the imported namespace), an unprefixed name the default
namespace, or no namespace when none is declared. Substitution-group members
stay in the target namespace (this schema's own globals).

The content model is shared between schema-restriction checks and instance
validation, so both improve. XSTS: valid schemas rejected 32 to 4, invalid
schemas accepted 526 to 520, valid instances rejected 214 to 180, invalid
instances accepted held at 160.

Disclosed limitations: a prefix re-bound on an inner element is not honored,
cross-namespace substitution-group members are not modeled, and a dangling
reference is not yet flagged.
2026-06-14 17:50:26 +02:00
68273205f6 feat(schema): full NameAndTypeOK type derivation in element restriction
Complete the element-vs-element type clause of Particle Valid (Restriction):
the restricting element's type must be, or derive by restriction from, the base
element's type. Previously only names were compared, so an unrelated, wider, or
ur-type element type was accepted under restriction.

Enrich the content-model particle: Term.element carries the element type's
derivation identity (the resolved type name, anyType for an absent type, nil for
an inline anonymous type). Record named simpleType restriction derivations in the
derivation table alongside complex ones. The element-vs-element check walks the
user restriction chain and then the built-in lattice to decide derivation by
name; an ur-type base, a union base (membership is not a restriction chain), and
an inline anonymous type are permitted, so no valid restriction is rejected.

XSTS: invalid schemas accepted 616 to 594, invalid instances accepted 165 to 160
(more invalid schemas rejected up front), valid schemas rejected held at 72,
valid instances rejected held at 214.
2026-06-14 13:47:18 +02:00
90ae16cba1 feat(schema): enforce Unique Particle Attribution (cos-nonambig)
Reject ambiguous content models at compile time via a Glushkov position
automaton over a literal, QName-resolved view of the raw schema tree, built
just for this rule and independent of the instance-validation content model.

The literal view resolves a ref's QName through the schema's prefix bindings
(not the blanket target namespace) and does not expand substitution groups,
removing the two false-overlap sources a check over the compiled model
produced. Determinism is per particle: a particle competing with a repetition
of itself is never a violation, so a fixed count {n,n} unrolls into copies in
sequence while a true range or unbounded repeat uses a self-loop, and only two
distinct source particles with overlapping labels conflict. The overlap test
is QName-only, an under-rejection that can never over-reject.

A position cap bounds pathological nesting (skipping the check, never
over-rejecting). The element-declarations-consistent test loses its
incidentally-ambiguous (a?, a) case in favor of a deterministic (a, b, a).

XSTS: invalid schemas accepted 884 to 864, valid schemas rejected held at 72,
instance buckets unchanged.
2026-06-14 09:35:33 +02:00
5ebf3b861c feat(schema): enforce component-name uniqueness
Within a schema, global type names (simpleType and complexType share one symbol
space), global element names, global attribute names, named model-group names,
and named attribute-group names must each be unique, and identity-constraint
names must be unique across the document. A duplicate was accepted, with the
later definition silently overwriting the earlier one.

Only the document's own globals are examined (xs:redefine children and unloaded
included documents are not direct globals), so a redefinition is not mistaken
for a clash; the symbol spaces are kept separate, so a type and an element may
share a name.

XSTS: invalid schemas accepted 909 to 891 (-18); valid schemas rejected held at
72; instance buckets unchanged.
2026-06-14 02:28:29 +02:00
cbeea0e3c2 feat(schema): reject unresolvable references at compile time
Every QName a schema names must resolve to a declared component or a built-in:
type/base/itemType/memberTypes to a type, element/attribute/group/attributeGroup
ref to the matching global component, and element substitutionGroup to a global
element. An undeclared reference was previously accepted at compile time and only
surfaced lazily during instance validation, if at all.

References are matched by local name, with the value trimmed (a QName attribute
is whiteSpace=collapse). The whole check is skipped when the document declares an
import/include/redefine, since the default compile does not load externals and
the pools would be incomplete; that guard is gated on the XSD namespace so a
foreign element named "import" does not trip it.

XSTS: invalid schemas accepted 1092 to 1051 (-41); valid schemas rejected held
at 72 (an interim version regressed one valid schema on a whitespace-padded
base="  xs:string ", fixed by trimming); instance buckets unchanged.
2026-06-14 00:17:29 +02:00
6147666e49 feat(schema): structural validity against the schema-for-schemas content model
The general mechanism for the structural tail of the schema-validity gap, in
place of hand-coding a rule per component. A data-driven table encodes the XSD
1.0 schema-for-schemas child content model; at compile time it rejects any
component child the model does not admit, more than one annotation (or an
annotation that is not first, except on schema/redefine, which allow several),
and an identity constraint missing its selector or field.

The walk visits only XSD-namespace elements and skips appinfo/documentation
foreign content, so foreign children are never flagged; restriction/extension
use the permissive union of their simple/complex/simpleContent contexts, so a
child valid in some context is never rejected. One table covers every component.

XSTS: invalid schemas accepted 1688 to 1313 (-375); valid schemas rejected held
at 72 (no over-rejection); instance buckets unchanged.
2026-06-13 22:48:20 +02:00
1249dd4c14 feat(schema): validate the id attribute (xs:ID) at compile time
The id attribute on any XSD component is of type xs:ID, so each value must be a
valid NCName and unique within the schema document. The values were never
checked, so a malformed (id="", id="123") or duplicated id left the schema
wrongly accepted across many categories (idA-idK, attribute groups, complex
types).

The schema-document tree is walked once, collecting id attributes. Two guards
keep valid schemas from being rejected: appinfo/documentation subtrees are
skipped (their foreign content carries its own non-xs:ID id attributes), and
only the unprefixed id is treated as the component identifier (a foreign xml:id
or pre:id is a different attribute). Findings join the shared parse-time
diagnostics.

XSTS: invalid schemas accepted 1789 to 1688; no other bucket raised.
2026-06-13 22:29:29 +02:00
d3daea5b91 feat(schema): validate constraining-facet definitions at compile time
First cut at the schema-validity gap: the validator ran instance-validity rules
but not schema-validity rules, so it silently accepted invalid schemas. Per XSD
Part 2 Datatypes 4.3, a length-family facet value (length, minLength, maxLength,
totalDigits, fractionDigits) must be a valid nonNegativeInteger (totalDigits a
positiveInteger), length may not co-occur with minLength/maxLength, and the
integer ranges must be ordered. Malformed values (length="", "-1", "a", "1e2")
were silently dropped by numeric facet parsing and the schema accepted.

The definition is now read raw from the restriction and validated at compile
time. Well-formedness is checked independently of machine-integer range (via a
canonical digit string), so a huge but valid value is not misreported as
malformed, and ordering compares by length then lexically. Findings travel
through a shared reference-type diagnostics collector on the value-type parsing
context (surviving its copies) and join the model-level consistency findings
thrown from Schema.Document.init.

Limited to facets declared on the local restriction, so an inherited facet is
never wrongly flagged. The min/maxInclusive value-space rules, facet
applicability per base, and inherited co-occurrence remain for later iterations
(see docs/schema-validity-burndown.md).

XSTS: invalid schemas accepted 2461 to 2082, no change to any other bucket.
2026-06-13 21:50:51 +02:00
b83948b09b fix(schema): allow character data in a mixed type with no content model
A complexType with mixed="true" and no sequence/choice/all (only attributes)
was compiled to the empty content type, which forbids text, so a mixed element
carrying only character data was rejected. It is now mixed content over an empty
particle: text is permitted, child elements are still disallowed. Non-mixed
empty types are unchanged.

XSTS: valid instances rejected 311 to 308. Baseline updated; regression tests
cover text accepted, an empty element, a child element rejected, and the
non-mixed control still rejecting text.

Refs #147, #129.
2026-06-13 19:37:24 +02:00
9ff7ed360f fix(schema): resolve <xs:attribute ref> to global attribute declarations
An <xs:attribute ref="..."> reference has no name attribute, so attributeUse
returned nil and the referenced attribute was dropped, making it look undeclared
on the instance (nested attributeGroup refs were already flattened, but the
per-attribute reference was not). References now resolve through a new global
attribute index to the declaration's type, with use/default/fixed taken from the
reference site and the name qualified to the target namespace.

XSTS: valid instances rejected 338 to 311. Baseline updated; regression tests
cover a referenced attribute admitted, its type enforced, and an attribute
outside the group still rejected. The attribute-parsing functions move to
XSDAttributeParser.swift to keep XSDParser within the type-body length limit.

Refs #147, #129.
2026-06-13 19:30:11 +02:00
43456029ac fix(schema): enforce element-level block against xsi:type substitution
The block on an element declaration (distinct from block on its type) was
parsed into the derivation tables but never threaded to the instance validator,
so an xsi:type that substitutes a type derived by a blocked method was accepted.
elementBlock now flows through XSDCompiled, SchemaDocument, the validation
context, and both ComplexValidator construction sites to blockedSubstitutionError,
which checks the element's own block alongside the type's. It also resolves the
element-ref type indirection (a ref'd child carries an `element:foo` key that
resolves through `types` to the concrete declared type) so the derivation chain
and block checks fire for referenced children, not only the document root.

XSTS: invalid instances accepted 261 to 240. Baseline updated; regression tests
cover element block rejecting a restriction xsi:type, the no-block control, and
an unrelated block not over-rejecting.

Refs #147, #129.
2026-06-13 18:04:25 +02:00
c8f91e329c refactor(xsd): schema-consistency checks through the validation framework
The derivation final check and Particle Valid (Restriction) move from
fail-fast throws at schema compile into composable Validation rules
(finalRespected, restrictionsAreSubsets) over per-type SchemaTypeFact
subjects, so a schema with several problems reports every finding at
once. SchemaError gains .inconsistent([String]) carrying all findings
(texts unchanged) and drops .finalViolation/.invalidRestriction.
checkRedefine, checkAllGroups, and notASchema stay throws: they examine
the raw schema source, the compilation analog of well-formedness.
2026-06-10 06:04:40 +02:00
2b7138ab61 feat(schema): XSD namespace qualification and xs:all constraints
Make XSD validation namespace-aware and enforce the form defaults and
xs:all structural constraints that were previously ignored:

- targetNamespace is tracked: a schema's global elements are in its
  target namespace, and an instance's root element must be in it.
- elementFormDefault / attributeFormDefault (and per-declaration form)
  qualify local element and attribute declarations. The content-model
  matcher already compares namespace URIs, so qualified declarations
  now match only same-namespace instance nodes; attribute matching
  became namespace-aware too.
- xs:all groups are checked at compile time: every member must be an
  element occurring at most once, the group may occur at most once, and
  nested model groups inside an all are rejected (a SchemaError).

When a schema has no target namespace the stamping is a no-op, so
unqualified schemas validate exactly as before.

Cross-namespace type references through xs:import resolve by local name
only; full multi-namespace type tables are not modeled.

Closes #78
2026-06-08 14:24:17 +02:00
4fad612281 feat(schema): XSD derivation control (abstract, block, final, redefine)
Read and enforce the XSD 1.0 derivation controls that were previously
ignored:

- abstract elements may not appear in an instance directly, only through
  a substitution member; an element of an abstract complex type must
  supply an xsi:type naming a concrete derived type.
- block on a type forbids the disallowed xsi:type substitutions; block on
  an element drops substitution-group members that derive by a blocked
  method.
- final on a type rejects, at compile time, any type that derives from it
  by a final method.
- xs:redefine requires the redefined type to derive from itself.

Track each named complex type's base and derivation method as the
backbone these checks walk. Also compose complexContent extension and
restriction content models, which were previously dropped: an extension
now inherits its base's attributes and content (base first, then the
extension, in sequence).

Complex-type restriction is not verified to be a structurally valid
subset of its base (Particle Valid (Restriction)); that limit is
documented at its sites.

Closes #76
2026-06-08 14:14:23 +02:00
b01499743c feat(schema): XSD wildcards and list built-in datatypes
Add xs:any and xs:anyAttribute wildcards with namespace and
processContents constraints (skip/lax/strict), and resolve the
list built-in datatypes (IDREFS, ENTITIES, NMTOKENS) to their
item types so each list item validates.

WildcardNamespace models ##any/##other/enumerated URI sets;
Wildcard.admits gates undeclared attributes and wildcard-matched
children. ContentNFA.follow exposes the follow set for completions.

Closes #77
2026-06-08 13:53:22 +02:00
4345e6170e feat(xsd): xsi:nil/nillable and fixed/default value constraints
Closes #75, on the validation framework. XSD instance-level features:

- nillable elements: an xsi:nil="true" element is accepted (its content model is
  not checked) only when the element is declared nillable and carries no content;
  xsi:nil on a non-nillable element, or a nilled element with content, is a
  located error.
- fixed value constraints: a present fixed attribute must equal its fixed value,
  and a fixed element's text must match.
- default/fixed are parsed onto AttributeUse (per use) and collected per element
  name (nillable set + value constraints), threaded through XSDContext to the
  ComplexValidator.

All findings are located ValidationErrors. Collection and helper methods move to
same-file extensions to stay within type-body limits.
2026-06-08 13:03:06 +02:00
fe19b0b1ea feat(xsd): add identity constraints (unique, key, keyref)
Validate XSD identity constraints over the instance document in a pass that
walks the tree alongside type validation:

- xs:unique requires the selected nodes' field tuples to be distinct, excluding
  any node that is missing a field.
- xs:key additionally requires every field to be present.
- xs:keyref requires each tuple to equal a tuple of the named key or unique
  visible in an enclosing scope, resolved through a stack of scope frames so a
  keyref and the key it refers to may sit on the same element.

The selector and field XPaths run on the existing XPath engine, relative to the
declaring element and each selected node. Constraints are gathered for every
element declaration at any depth, keyed by element name.

Closes #67
2026-06-08 08:58:36 +02:00
4a966260f3 feat(xsd): add include/import/redefine, substitution groups, and xsi:type
Compose schemas across documents and resolve types at the instance:

- xs:include, xs:import, and xs:redefine pull a transitive closure of schema
  documents through an injected loader (resolved by schemaLocation), base
  schemas first so the including schema wins; a redefine's nested definitions
  override the schema it includes.
- substitution groups: an xs:element ref to a head also admits every element
  that substitutes for it, expanded at parse time into a choice over the head
  and its transitive members.
- xsi:type overrides an element's declared type with the named type at the
  instance; XML Schema instance attributes are exempt from attribute checks.

The schema parser splits across XSDContext (shared tree helpers, document
collection, parsing context), XSDSimpleParser (simple types and facets), and
XSDParser (elements, complex types, model groups) to keep each within bounds.

Closes #66
2026-06-08 08:46:24 +02:00
cadc63ba8b feat(xsd): add list/union simple types, attribute groups, and element/group refs
Extend the schema parser toward full structural parity:

- list simple types validate each whitespace-separated item against the item
  type and count items for the length facets; union simple types admit a value
  valid for any member type. A restriction over a named list or union inherits
  its variety.
- Named attribute groups are expanded (transitively) into every complex type
  that references them.
- Named model groups are expanded at each xs:group ref, with a cycle guard;
  element refs resolve to the referenced global element's declared type,
  through a reserved type-table key so forward and recursive refs settle at
  validation time.

Named simple types now resolve over repeated passes so they may restrict or
list one another regardless of document order. Simple-type and facet parsing
moves into a sibling XSDSimpleParser to keep each parser within bounds.

Closes #65
2026-06-08 08:28:17 +02:00
8ae3c79f42 feat(schema): XSD schema-document parser and end-to-end validation
Add Schema.Document compiling an xs:schema into global element declarations
and a named-type table, and validate(_:) checking instance documents end to
end. The parser supports global and local element declarations, named and
inline simple and complex types, sequence/choice/all groups with occurrence,
attribute uses, simple content (restriction/extension), the full facet set,
and recursive types via named references resolved during validation. Extends
ComplexType with a typeReference case and ComplexValidator with a type table
so recursive schemas are representable.

Closes #10
2026-06-08 06:40:03 +02:00