Commit Graph

52 Commits

Author SHA1 Message Date
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
7c059e1355 fix(schema): reject circular model-group, attribute-group, and substitution-group references
A model group may not contain itself (mg-props-correct.2), an attribute group
may not reference itself transitively (ag-props-correct.3), and a substitution
chain may not loop (e-props-correct.6).

The model-group check follows references only within a definition's own
compositor tree, stopping at element and type boundaries: a group referenced
inside an element's content is a recursive data structure (permitted), not the
group containing itself (particlesZ010, addB077). A reference is followed only
when it resolves to the schema's own target namespace; a group or attribute
group redefined in xs:redefine ends the chain (its single self-reference is the
legal redefinition).

XSTS: invalid schemas accepted 516 to 505 (attgC010/020/031, attgD015,
groupB012-015, addB180, xsd009.e, xsd010.e now rejected), no other bucket
raised. A genuine cycle introduced across two redefined definitions is a
disclosed under-rejection (an adversarial critic's only finding).
2026-06-15 01:50:35 +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
81be470512 docs(rules): research-first rule, consult research repo and literature before guessing 2026-06-15 00:30:01 +02:00
119894a5a2 fix(schema): implement MapAndSum (sequence restricting a choice) in particle restriction
The W3C "Particle Valid (Restriction)" MapAndSum rule (XSD 1.0 §3.9.6): a derived
sequence restricting a base choice is valid iff each derived particle restricts
some base branch AND the derived sequence's occurrence count times its number of
particles lies within the base choice's occurrence range.

PureXML applied the same-compositor occurrence subsumption (the sequence's own
range against the choice's), which both rejected a valid restriction
(sequence{2,4}(e1,e2) of choice{3,9}(e1,e2): 4..8 elements fit 3..9) and accepted
an invalid one (a two-member sequence restricting a single-occurrence choice,
count-product 2 > 1). The count-product is the spec's measure, matched to the
reference implementation (checkMapAndSum/checkOccurrenceRange).

The (sequence, choice) branch in groupValid is retained: it is reached by the
(element, group) RecurseAsIfGroup path, which wraps an element as a one-particle
sequence against a base choice.

XSTS: valid schemas rejected 2 to 1, invalid schemas accepted 520 to 519, no
other bucket raised. The remaining valid-rejected case (particlesZ001) is a
documented spec ambiguity (RecurseAsIfGroup), recorded in docs/xsts-deviations.md.
2026-06-14 23:46:37 +02:00
9a1bb652a7 fix(schema): content-free restriction of emptiable base, substitution-group declaration order
Two more Particle Valid (Restriction) over-rejections.

A derived particle that can never contribute a child (its own maxOccurs="0", or
a group whose every member is content-free) accepts only the empty sequence, so
it validly restricts any emptiable base. Checked before the structural cases,
which previously recursed into the members of a maxOccurs="0" group and rejected
it for their own occurrences (particlesW006).

Substitution-group expansion now lists members in declaration order: the closure
walk used a popLast() stack that reversed them, so a reference to a head expanded
to choice(head, m2, m1) and an order-preserving RecurseLax restriction by
choice(m1, m2) could not map (elemZ027a). The choice/choice restriction stays
order-preserving, as the W3C suite requires (choice(b|a) is not a valid
restriction of choice(a|b), particlesT002); only the expansion order is fixed.
The expansion is shared with instance validation and the determinism check, both
order-neutral.

XSTS: valid schemas rejected 4 to 2, no other bucket raised. The pinned XSTS
baselines (never asserted under a plain swift test, the suite being opt-in) are
corrected to the measured 2/520/180/160.
2026-06-14 22:48:12 +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
aba91a168f fix(schema): normalize content model, scope occurrence subsumption in particle restriction
Particle Valid (Restriction) derives its pairwise check over the spec's
normalized content model: pointless particles are eliminated (maxOccurs=0
members dropped, a {1,1} single-member group unwrapped, a nested
same-compositor {1,1} group spliced), all language-preserving. The outer
occurrence-range check was wrongly applied to the cross-kind pairings, where
it compares a derived particle's own range against a base group's or
wildcard's. It is now applied only to the same-kind pairings (element/element,
wildcard/wildcard, group/group) and element/wildcard; element/group
(RecurseAsIfGroup), group/element, and group/wildcard match occurrence inside
the case, against a member or as an effective total range.

A group restricting a wildcard (NSRecurseCheckCardinality) is valid when every
leaf is admitted and the effective total occurrence range is within the
wildcard's; a group restricting a single element is bounded the same way, by
that element's name, type derivation, and occurrence range.

XSTS: valid schemas rejected 45 to 32, invalid schemas accepted 528 to 526,
no other bucket raised.
2026-06-14 17:18:38 +02:00
5aeb2aca4c docs: production-readiness standard, read every conformance iteration
The bar for shipping PureXML in an IDE: correctness total and characterized,
false positives at zero, not "passes the suite." Four stoppers (reject no valid
input; accept no faulty input; characterize correctness via differential + fuzz,
no silent under-rejection debt; interactive safety: bounded resources, located
recoverable errors) plus a per-iteration checklist and definition of done.

Wired into the per-iteration protocol (schema-validity-burndown.md) and the
session Read First list (CLAUDE.md) so it is read every iteration.
2026-06-14 16:28:03 +02:00
87cc83d0ce fix(schema): stop Particle Valid (Restriction) over-rejecting valid schemas
The restriction subset check rejected 72 valid schemas (#148). Three
language-preserving corrections:

- Remove pointless particles (maxOccurs=0) from both the derived and base
  content before mapping, so they no longer consume a base particle and starve
  the particles after them (e.g. (e1 maxOccurs=0, e2) restricting (any)).
- A content-free restriction (an empty sequence, or a group whose members all
  cannot occur) is a valid restriction of an EMPTY base: both accept only the
  empty sequence.
- A derived group that reduces to empty after that removal validly restricts its
  base only when the base is itself emptiable (emptiable(base) accounts for the
  base's own occurrence), closing the vacuous-acceptance gap the removal would
  otherwise open in the MapAndSum and choice paths.

XSTS: valid schemas rejected 72 to 45. invalid schemas accepted 527 to 528 from
addB32, whose only invalidity is a malformed imported document not yet loaded
(#161); the restriction itself is valid, so accepting it is correct.
2026-06-14 15:56:04 +02:00
dd10f32abb feat(schema): a simpleType must have exactly one of restriction/list/union
Per XSD 1.0 Datatypes a simpleType's content is
(annotation?, (restriction | list | union)). The membership table admitted each
name without the cardinality, so an empty simpleType or one combining two
derivations (two unions, a union and a list, two restrictions) was accepted.
The exactly-one rule is now enforced, mirroring the named-group compositor check.

XSTS: invalid schemas accepted 537 to 527, valid schemas rejected held at 72,
instance buckets unchanged.
2026-06-14 14:30:25 +02:00
11c4eacdff feat(schema): validate final/block derivation-control value space
The final and block attributes are #all or a whitespace list of method tokens,
but the admitted tokens depend on the component: element final is
extension/restriction, element block adds substitution, complexType final/block
are extension/restriction, simpleType final is list/union/restriction. The
tokens were parsed leniently (unknown ignored, substitution admitted
everywhere), so final="substitution" on an element, a misspelled or miscased
token, or #all mixed with other tokens was accepted.

The list is validated against the component's exact set; finalDefault/
blockDefault (the wider schema-level value space) and foreign attributes are
left alone.

XSTS: invalid schemas accepted 580 to 537, valid schemas rejected held at 72,
instance buckets unchanged.
2026-06-14 14:19:10 +02:00
6a7fb1d594 feat(schema): enforce simpleContent derivation content order
A simpleContent restriction is (annotation?, (simpleType?, facet*),
(attribute|attributeGroup)*, anyAttribute?) and an extension is
(annotation?, (attribute|attributeGroup)*, anyAttribute?): no model group, no
facets/simpleType in an extension, attributes before a single anyAttribute. The
membership table admitted the names but not the order or the model-group ban, so
a model group inside simpleContent, a facet in an extension, a misordered facet
or attribute, or a second anyAttribute (the ctD family) was accepted.

The derivation's children are matched against the context's ordered slot model,
reusing the slot walk built for complexContent. The structure walk's
per-component dispatch is factored into a helper to keep its complexity bounded.

XSTS: invalid schemas accepted 594 to 580, valid schemas rejected held at 72,
instance buckets unchanged.
2026-06-14 14:05:49 +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
711a50b8ea feat(schema): reject ur-type widening in element restriction
Extend NameAndTypeOK: restricting a concrete-typed element to an ur-type
(xs:anyType / xs:anySimpleType / xs:anyAtomicType) is widening, hence an invalid
restriction. The element-vs-element check now rejects it before the atomic
derivation check.

The ur-type is recognised by a type reference whose key is anyType /
anySimpleType / anyAtomicType AND which is absent from the named-type table, so
a user type that merely shares the local name (in another namespace) is not
mistaken for the ur-type. The reverse (an ur-type base narrowed by the
restriction) stays valid.

XSTS: invalid schemas accepted 620 to 616, valid schemas rejected held at 72,
instance buckets unchanged.
2026-06-14 13:07:31 +02:00
7a80410945 feat(schema): check element type in Particle Valid (Restriction)
When a restriction maps a same-named element onto a base element, the
restricting type must derive from the base's (NameAndTypeOK). Renaming an
xs:string element to xs:int under restriction was accepted because the
element-vs-element check compared only names.

ParticleRestriction now threads the compiled type table through its recursion;
the element-vs-element case resolves both element types (following a type
reference through the table) and, when both are atomic simple types, requires
the restricting type's built-in base to derive from the base's. A complex,
list, union, untyped, or unresolvable type is left alone, so no valid
restriction is rejected.

XSTS: invalid schemas accepted 621 to 620, valid schemas rejected held at 72,
instance buckets unchanged.
2026-06-14 12:29:23 +02:00
3e3b937e89 feat(schema): validate built-in-typed default/fixed values
An element/attribute default/fixed value must be a valid value of its type
(Attribute/Element Locally Valid, XSD 1.0). A fixed="abc" on an xs:integer or a
default="1 2" on an xs:int was accepted because the value constraint was never
checked at compile time.

The value is now validated against its type when the type resolves to a
built-in datatype (namespace-aware: only a type in the XSD namespace),
normalising whitespace per the type so a default=" 5 " on an integer stays
valid. A named or inline user type is left to its own rules, so no valid value
constraint is rejected.

XSTS: invalid schemas accepted 638 to 621, valid schemas rejected held at 72,
instance buckets unchanged.
2026-06-14 11:55:55 +02:00
0143381f67 feat(schema): enforce facet applicability to simple-type variety
A constraining facet must apply to its simple type's variety (XSD 1.0 Part 2):
a list may be constrained only by length/minLength/maxLength, pattern,
enumeration, whiteSpace; a union only by pattern and enumeration. A value-bound
facet (maxInclusive, totalDigits, ...) constrains an atomic value space, so
restricting a list of integer by maxInclusive was wrongly accepted.

The base's variety is resolved from the raw tree (this runs before the types
are compiled), namespace-aware so a user type sharing a built-in's local name
does not flip the variety: a base in the XSD namespace is a built-in (only the
list datatypes are list-variety), a base in the schema's own target namespace
resolves to a declared global, and an external or unresolvable base is skipped.

XSTS: invalid schemas accepted 664 to 638, valid schemas rejected held at 72,
instance buckets unchanged.
2026-06-14 11:36:21 +02:00
9d4ce304c2 feat(schema): enforce complexType shorthand content order
A complexType's shorthand content (no simpleContent/complexContent) has the
model (annotation?, (group|all|choice|sequence)?, (attribute|attributeGroup)*,
anyAttribute?). The existing content-shape check caught the wrong number of
model groups and content-spec exclusivity, but not the order, so a model group
after an attribute, an attribute after anyAttribute, or a second anyAttribute
(the ctB family) was accepted.

The complexType's direct children, excluding annotation and the content specs,
are matched against the same ordered slot model used for a complexContent
derivation.

XSTS: invalid schemas accepted 687 to 664, valid schemas rejected held at 72,
instance buckets unchanged.
2026-06-14 11:15:24 +02:00
e016775a82 feat(schema): validate wildcard namespace constraint value space
An any/anyAttribute namespace constraint is ('##any' | '##other') standing
alone, or a list of namespace URIs and the ##targetNamespace/##local tokens.
The wildcard parser mapped any unrecognised token to a literal URI, so a
misspelled token (##all, ##target), an ##any/##other inside a list, or any
other ##-prefixed token was accepted.

The unprefixed namespace value is now tokenised and checked against that
grammar, with ##any/##other admitted only as the whole value. A prefixed
(foreign) attribute sharing the local name is the author's own and is not
checked, so no valid schema is rejected.

XSTS: invalid schemas accepted 736 to 687, valid schemas rejected held at 72,
instance buckets unchanged.
2026-06-14 11:00:45 +02:00
cfaf9b7dd1 feat(schema): validate identity-constraint selector/field XPath subset
An identity constraint's selector and field xpath must be in the XSD 1.0
restricted-XPath subset (Part 1, 3.11.6), far narrower than full XPath: a
leading optional .//, then /-separated steps, a step being '.', a name test
(QName, '*', or NCName:*), or a child:: axis; a field path may end in an
attribute step (@x or attribute::x). No predicates, no other axes, no absolute
or interior //. The xpath was stored raw and compiled lazily with try?, so a
malformed one was silently accepted.

It is now lexed into a whitespace-tolerant token stream and parsed against the
grammar: whitespace between tokens is allowed (child :: a, . //.) while // must
be adjacent and a name test's own ':' may not be spaced. The name lexer uses
the XML NameChar set so a non-ASCII NCName is accepted.

The two identity tests that fed a malformed xpath and expected a
validation-time located error now assert the stronger compile-time rejection.

XSTS: invalid schemas accepted 785 to 736, valid schemas rejected held at 72,
instance buckets unchanged.
2026-06-14 10:30:06 +02:00
8a7445c5bb feat(schema): enforce complexContent derivation content order
A restriction or extension inside complexContent has the content model
(annotation?, (group|all|choice|sequence)?, (attribute|attributeGroup)*,
anyAttribute?). The allowedChildren membership table admitted the right child
names but not their order or cardinality, so a second model group, a model
group after an attribute, an attribute after anyAttribute, or a second
anyAttribute was accepted.

A forward-only walk matches the children against an ordered slot model (one
model group, unbounded attributes, one anyAttribute). The check runs only in
the complexContent context, where a model group belongs to the model, so a
simpleContent derivation (facets, no model group) keeps its own rules and no
valid schema is rejected.

XSTS: invalid schemas accepted 864 to 785, valid schemas rejected held at 72,
instance buckets unchanged.
2026-06-14 09:56:04 +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
42a87b132c feat(schema): resolve keyref refer to a declared key or unique
A keyref's refer must name a key or unique constraint. A refer pointing at a
non-existent constraint was accepted. keyrefReferErrors collects all key/unique
names and all keyref refers across the document (so a forward refer, textually
before its key, is handled), then flags any refer with no matching name. Matched
by local name, like the rest of the compiler. Skipped when the document declares
import/include/redefine, whose externals the default compile does not load.

XSTS: invalid schemas accepted 891 to 884 (-7); valid schemas rejected held at
72; instance buckets unchanged.
2026-06-14 08:04:34 +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
243a6dbc9b feat(schema): enforce Element Declarations Consistent (cos-element-consistent)
Within one complex type's content model, two element declarations with the same
name must have the same type definition. Two same-name elements with different
types were accepted. elementDeclsConsistentErrors collects element particles
across the content model -- descending through model groups and the content-
derivation wrappers, but never into an element's own nested type, which is a
separate content model -- and flags a name seen with more than one distinct type
key (a type reference by local name, a distinct token per inline type, or the
untyped ur-type).

Modeled clean-room on Xerces (libxml2 enforces this only partially). Substitution-
group expansion is not folded in, an accepted under-rejection.

XSTS: invalid schemas accepted 925 to 909 (-16); valid schemas rejected held at
72; instance buckets unchanged.
2026-06-14 01:47:43 +02:00
39222ca760 feat(schema): enforce schema-vocabulary attribute applicability
The attribute sibling of the content-model table. Each XSD component admits a
fixed set of attributes; an unprefixed attribute outside that set (a typo like
nullable, a stray foo, name on a sequence) was accepted because nothing checked
attribute applicability. allowedAttributes now lists each component's admitted
unprefixed attributes (the permissive union of global and local use), and
attributeApplicabilityErrors rejects the rest, plus ref co-occurring with name
or type.

Foreign (prefixed) attributes and namespace declarations are the schema author's
own and are not checked, so no valid schema is rejected.

XSTS: invalid schemas accepted 956 to 925 (-31); valid schemas rejected held at
72; instance buckets unchanged.
2026-06-14 01:18:03 +02:00
b37e97b5a9 feat(schema): a named group must contain exactly one model group
XSD 1.0 gives a named model-group definition's content as
(annotation?, (all | choice | sequence)) -- exactly one compositor. The
allowedChildren set-membership check allowed any number, so an xs:group with two
compositors, or with none, was accepted. namedGroupErrors now enforces the
cardinality, gated on the node being a definition (has a name) so a group
reference is never flagged.

XSTS: invalid schemas accepted 977 to 956 (-21); valid schemas rejected held at
72; instance buckets unchanged.
2026-06-14 01:04:25 +02:00
09a0f9bf5c feat(schema): reject a particle whose minOccurs exceeds its maxOccurs
An occurrence range like minOccurs="5" maxOccurs="2" is empty and illegal in
XSD, but the validator accepted it (only the individual values were checked).
occurrenceOrderErrors now compares the two on a particle that carries both: the
comparison is by canonical magnitude (sign and leading zeros stripped, length
then lexicographic), so it holds beyond a machine integer; maxOccurs="unbounded"
is never exceeded and an equal single-point range is allowed.

XSTS: invalid schemas accepted 997 to 977 (-20); valid schemas rejected held at
72; instance buckets unchanged.
2026-06-14 00:47:51 +02:00
4d9098ad60 docs(schema): note complexType content-shape rule in burndown plan 2026-06-14 00:33:36 +02:00
6fbc92593e feat(schema): enforce the complexType content-model shape
XSD 1.0 allows a complexType's content to be simpleContent, or complexContent,
or a model group followed by attributes, never a mix. The allowedChildren
set-membership check permitted illegal combinations, so a complexType with two
simpleContent, simpleContent beside a sequence, or two model groups was accepted.

complexTypeContentErrors now enforces, over the complexType's direct children:
at most one of simpleContent/complexContent; when one is present, no direct model
group and no direct attribute (those belong inside the extension/restriction);
and at most one model group. Only direct children are examined, so attributes
nested inside a derivation are not flagged.

XSTS: invalid schemas accepted 1051 to 997 (-54); valid schemas rejected held at
72; instance buckets unchanged. Also folds in a behavior-identical lint cleanup
of the reference-walk external-definition check.
2026-06-14 00:32:54 +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
62e53b0fc1 feat(schema): validate pattern facet regular expressions at compile time
Each pattern facet value is now compiled when the schema is, instead of only
lazily at instance time (try?) where an unparseable pattern was silently
ignored. A structurally malformed pattern (unbalanced parentheses, a quantifier
with nothing to repeat) is rejected.

The check is conservative against over-rejection: it rejects only the
unambiguous structural RegexError kinds (.unbalanced, .danglingQuantifier) and
tolerates the kinds that fire on otherwise-valid patterns the engine cannot
fully handle (.empty for the empty pattern, .unsupported for an untabulated
\p{Is...} block such as a surrogate or historic block, .badQuantifier for the
lenient {,m} form, and .badEscape/.badClass which could fire on valid character-
class subtraction). So a{2,1} and a trailing backslash remain accepted (an
under-rejection), but no valid schema is rejected.

XSTS: invalid schemas accepted 1129 to 1092 (-37); valid schemas rejected held
at 72; instance buckets unchanged.
2026-06-13 23:52:11 +02:00
ca8fd27877 feat(schema): check name and reference attribute lexical validity
Extends the compile-time attribute-model check. A component's name must be a
valid NCName, the reference attributes type/base/ref/itemType/refer/
substitutionGroup must be lexical QNames, and memberTypes a list of QNames.
These were unchecked, so name="", name="123", name="a:b", or type=":_" left the
schema wrongly accepted.

Only the lexical form is checked (a QName's prefix is resolved elsewhere), so a
valid reference is never rejected; each of these attribute names has one type
throughout XSD, so checking by local name is safe.

XSTS: invalid schemas accepted 1190 to 1129 (-61); valid schemas rejected held
at 72 (no over-rejection); instance buckets unchanged.
2026-06-13 23:16:47 +02:00
8011f3d2e8 feat(schema): validate schema-vocabulary attribute values at compile time
The attribute-model sibling of the structural content-model check, folded into
the same schema-document walk. Enumerated attributes (form,
elementFormDefault, attributeFormDefault, use, processContents, mixed, abstract,
nillable) and the occurrence attributes (minOccurs a nonNegativeInteger,
maxOccurs a nonNegativeInteger or unbounded) are checked against their fixed
value space. An out-of-space value (use="foo", mixed="yes", minOccurs="-1") was
previously ignored and the schema accepted.

Each of these names has one meaning throughout XSD, so checking by local name is
safe. Only unprefixed attributes on XSD-namespace elements are checked, so a
foreign attribute is never flagged; values are trimmed before comparison.

XSTS: invalid schemas accepted 1313 to 1190 (-123); valid schemas rejected held
at 72 (no over-rejection); instance buckets unchanged.
2026-06-13 23:01:42 +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
aa7cbb71f8 feat(schema): validate value-bound facet definitions; accept gMonth --MM--
Continue the facet-validity work. Per XSD Part 2 Datatypes 4.3, each
minInclusive/maxInclusive/minExclusive/maxExclusive and enumeration value must
be a valid value of the base type; minInclusive excludes minExclusive and
maxInclusive excludes maxExclusive; and the lower bound may not exceed the upper
bound (nor equal it when either side is exclusive, an empty value space),
compared in the base primitive's value space. Bound and enumeration values were
previously unchecked, so a malformed or out-of-space value (maxInclusive="",
enumeration="" on float) was accepted.

The bound-value check (which validates a bound against its base type) exposed a
gMonth lexical gap: only --MM was recognized, not the XSD 1.0 form --MM-- used
by valid schemas across the corpus. parseMonthOnly now accepts an optional
trailing -- (the --MM erratum form still works), with an optional timezone,
which removes 26 false schema rejections the bound check would otherwise have
caused and also fixes instance-side gMonth validation.

XSTS: invalid schemas accepted 2082 to 1789; valid instances rejected 233 to
214 (the gMonth fix); valid schemas rejected held at 72; invalid instances
accepted held at 165.
2026-06-13 22:10:57 +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
dc929774c3 docs(schema): schema-validity burndown plan and per-PR critic protocol 2026-06-13 21:32:51 +02:00
4bd23d8286 feat(schema): built-in xsi:type resolution gated by substitution validity
Resolve a built-in datatype named by xsi:type (xsd:int, xsd:decimal,
xsd:string), which previously resolved only against schema-declared types and
so reported "unknown" and wrongly rejected the element. The built-in is now
resolved when it validly substitutes for the declared type per Schema-Validity
Assessment: the ur-type (anyType/anySimpleType) admits any type, and a bare
built-in declared type admits any built-in derived from it through the XSD
Part 2 datatype lattice (new BuiltinType.derives(from:)). An invalid built-in
substitution is a located error, not a silent acceptance.

Resolving the override correctly exposed two pre-existing gaps, both closed so
the change carries no invalid-instances-accepted regression:

- A simple-typed element rejects non-schema-instance attributes. A simple type
  declares no attribute uses, so an element of simple type (including via an
  xsi:type override, and including an xsi:nil element) may carry only xsi:* and
  namespace attributes. Checked before the nil short-circuit.
- Identity-constraint fields compare in their value space when the selected
  node carries an xsi:type. unique/key/keyref tuples compared lexically, so two
  xsi:type="xsd:decimal" fields 3.0 and 3 were treated as distinct and a unique
  violation was missed. Each field value now carries the type read from the
  node's xsi:type and compares via SimpleType.valueMatches when both share a
  built-in base, otherwise lexically.

XSTS: valid instances rejected 248 to 233; invalid instances accepted held at
165. Identity tests corrected to spec-valid schemas (an element carrying an
attribute must be a complex type, not type="xs:string").
2026-06-13 21:21:35 +02:00
a13e3e6645 docs(xsts): root-cause the remaining deviations after twelve fixes
Updates the worklist to the current settled-expectation baseline
(72/2461/308/165) and records the twelve fixes landed. The remaining instance
deviations are root-caused individually: the particle clusters need
multi-document schema loading and xsi:schemaLocation honoring under a strict
wildcard (a new capability, the largest single lever); reI is pattern-attribute
whitespace normalization; test/elemT are heterogeneous grab-bags needing
per-case triage; the rest are identity-XPath and wildcard/simple-type edges.
The clean single-root-cause fixes are exhausted; the tail is deliberate work.

Refs #129, #122.
2026-06-13 19:54:12 +02:00
7f82627403 test(xsts): count only settled expectations, excluding queried/disputed entries
The runner read <expected validity> but ignored <current status>, so it counted
disagreements with W3C-disputed expectations as deviations. The 2006-11-06
archive marks 748 instanceTests (plus schemaTests) status="queried" with the
expected validity contested in W3C Bugzilla (for example reJ11.i: \p{Lu}* over
two uppercase letters, prior expectation valid, current invalid+queried, where
PureXML's accept is defensible). Those are now skipped: neither agreement nor
deviation, since the expectation is not authoritative.

Recalibrated baseline (settled expectations only): valid schemas rejected 74 to
72, invalid schemas accepted 2467 to 2461, valid instances rejected 369 to 338,
invalid instances accepted 210 to 179. About 70 apparent deviations were
disagreements with contested expectations, never bugs.

Refs #129, #122.
2026-06-13 19:17:34 +02:00
c6305c9d93 fix(schema): treat QName length facets as non-constraining
XSD 1.0 Datatypes 4.3.1 defines the unit of length only for string, anyURI,
binary, and list; for QName it is implementation-defined. We had counted
characters, rejecting valid QName values. Like Xerces and the XSTS NIST set we
now treat length/minLength/maxLength on QName as always satisfied. Lexical
QName validity is still enforced.

The suite's contributors disagree here (NIST: non-constraining; Microsoft: a
character count), which the spec does not resolve, so a single behavior cannot
satisfy both. Net XSTS: valid instances rejected 547 to 502; 4 Microsoft cases
move to invalid instances accepted (257 to 261), recorded as a deliberate
interpretation in docs/xsts-deviations.md.

Refs #147, #129.
2026-06-13 18:04:25 +02:00
1480453125 docs(xsts): partition deviations by root cause and kind
Turns the raw failure list into a worklist driven by understanding each rule,
not by chasing the count. Three categories: (A) genuine correctness bugs in
instance validation worth fixing (union restriction facets ~200, QName length
~36, regex Unicode blocks ~155, duration ordering ~50, particles/int facets);
(B) schema-for-schemas structural validity, the bulk of the 2467 invalid
schemas accepted, unimplemented by design and documented as a deliberate
exclusion; (C) arguable cases needing investigation. Records the evidence
sampled per cluster.

Refs #145, #146, #147, #148.
2026-06-13 18:04:25 +02:00
9e345ea0fe feat(xpath,parsing): public streaming surface, linear document-order at scale
EventReader.next(), both initializers, and the Event alias are public
(an internal typealias in a split file had been shadowing the public
Parsing.Event); a new PureXMLPublicAPITests target compiles without
testable import so facade gaps fail CI. The large-corpus benchmarks
exposed document-order sorting as quadratic on flat fan-outs; the fix's
first attempt was itself worse (an eager sibling table built per
predicate evaluation), caught by sampling the live run, and replaced
with a lazy table: single-node sets never touch order keys and a
parent's table builds on its second lookup. XPath at 42 MB drops
27.1 s to 1.57 s (190x to 11.7x); at 210 MB PureXML completes in 9.4 s
the query libxml2 refuses at its 10M node-set cap (#143 tracks our
configurable budget). Benchmark harness: size series to 210 MB, CORPUS
mode for real files (NASA, SwissProt measured), SAX stage,
refusal-aware table, NULL-safe C driver. All conformance suites,
Linux, and WASM verified. Closes #142.
2026-06-11 06:06:31 +02:00
72eaeeb439 perf(parsing): byte-backed Reader and bulk content runs
String input copies its UTF-8 once into owned storage (an unsafe
pointer encapsulated in a class lifetime, never escaping the parser)
and scalars decode straight off the pointer, eliminating the
per-character closure chain; ASCII literals match and consume at the
byte level; and plain-ASCII character data scans as a single byte run
with one String built per run. Parse drops 32 percent cumulative
(0.49 s to 0.34 s on the 4.2 MB corpus, libxml2 ratio 30x to 20x).
The W3C suite caught one boundary (a run starting with '>' could
swallow the ]]> check after the character path consumed ]]); the run
scanner leaves a leading '>' to the character path, pinned as a unit
regression. All conformance suites, Linux, and WASM verified.
2026-06-10 18:48:57 +02:00
0ce59c826b perf(parsing): allocation-free Reader.matches(), first measured pass
matches() ran on every scan decision and materialized an Array of the
literal per call, the hottest comparison in the parse profile. It now
walks the literal directly. Parse time on the 4.2 MB benchmark corpus
drops 16 percent (0.49 s to 0.41 s, libxml2 ratio 30x to 26x); profile
findings and the optimization log recorded in docs/benchmarks.md.
2026-06-10 18:21:08 +02:00
8e1415aa64 feat(bench): PureXML vs libxml2 benchmark harness with first baseline
scripts/benchmark.sh generates a deterministic corpus, builds PureXML
in release and a C driver against the SDK's libxml2, times parse,
serialize, and XPath over the same bytes (best-of-N, internal timing,
IO excluded), cross-checks the same XPath count on both sides, and
prints the comparison. First honest baseline recorded in
docs/benchmarks.md: parse ~30x, xpath ~36x, serialize ~5x slower than
libxml2 on a 4.2 MB corpus; these are the numbers #139's optimization
passes burn down. The SAX stage isolation is blocked on #142 (the
public streaming surface is internal, found by this harness).
2026-06-10 18:16:09 +02:00
d2a9ba576b ci: split workflows for truthful badges and attribution gates
Replace monolithic ci.yml with per-platform workflows so each README badge
tracks one job. Scan commit messages for tool-agnostic AI attribution in CI,
sync local hooks to the canon, and refresh README status plus git-discipline Rule 8.
2026-06-08 04:37:32 +02:00