Commit Graph

6 Commits

Author SHA1 Message Date
380160cc2b refactor(validation): decompose XSD instance validation into composable rules
The two monolithic XSD content/identity rules only satisfied the OpenAPIKit
validation idiom at the wrong granularity: no per-constraint removability,
no isolable failing/near-miss tests. Split them into 32 named rules, each
finding tagged at its origin and cached per XSDContext so the walk still
runs once per document (the #101 decision against duplicating XSD's
recursive type resolution still holds). Wired the streaming path's ID/IDREF
and identity checks through the same framework instead of calling the
validators imperatively, and added the unwrapAndLookup combinator the rule
already named as required.

Also includes the mandated repo-wide swiftformat/swiftlint pass.
2026-07-12 14:12:17 +02:00
6f8222a840 feat(schema): compositional UPA determinism, delete positionCap
The Unique Particle Attribution check no longer inlines group references or caps.
A new CompositionalDeterminism engine computes first/followlast over particle
identities and summarizes each xs:group once, so a multiply-referenced nested
group whose inlined Glushkov automaton would be 2^K positions is checked in
O(nodes * particles^2) time. ContentModelDeterminism now delegates to it and the
inlining position automaton plus positionCap are deleted, closing the last named
interactive-safety resource-bound blocker (production-readiness stopper #4) and
the silent under-rejection the cap caused (stopper #3).

The compositional engine was proven verdict-equivalent to the prior inlining
automaton by a differential over the entire XSTS corpus (zero divergences) before
replacing it. The followlast detail was essential: a first attempt that emitted a
repeating end-item's loop-back and the suffix-first as separate decision sets
under-detected 13 XSTS content models; the differential caught every one.

Verification: XSTS gate unchanged (invalid-schemas-accepted 15, valid-schemas-
rejected 0, instance buckets held); SchemaDeterminismGroupBlowupTests compiles a
2^20-position model without hanging and rejects an ambiguous one the old cap would
have skipped (WASI uses a smaller depth, native time limits gated off WASI); full
suite, style, namespacing, and WASM all pass. The per-particle model and QName-only
overlap are preserved, so the false-positive direction stays at zero.
2026-06-22 22:19:44 +02:00
0a76658a28 fix(schema): check UPA over an extension's effective content model
Unique Particle Attribution checked only a complexContent extension's own
particles, missing the cross-boundary non-determinism with its base: the
effective content of an extension is the base type's content followed by the
extension's, so a base particle that can recur or end optionally competes
with the extension's leading particles. An element matching either was a UPA
violation the single-node check did not see, and the schema was wrongly
accepted (XSTS particlesZ022).

Build the content model over the effective sequence (base content prepended,
resolved recursively). The base is followed only for a local
(target-namespace), non-cyclic base chain; a foreign, built-in (xs:anyType),
unresolved, or cyclic base leaves the check on the extension's own content, so
no model that cannot be fully assembled is rejected (an under-rejection, never
a false positive). For a non-extension the effective list is a single node, so
sequence([x]) reproduces the prior automaton exactly.

Verified against the full W3C XSTS 2006-11-06 suite: invalid-schemas-accepted
31 -> 29 (particlesZ022 and one more), valid-schemas-rejected held at 0, no
other bucket moved. Adds UPA tests for an ambiguous extension and a
deterministic one (the over-rejection guard); moves the content-model-node and
effective-model helpers to a new file to stay within the type-body and
file-length budgets.
2026-06-21 22:12:32 +02:00
9a2cb7eab3 fix(schema): detect UPA overlap of ##other wildcards
Unique Particle Attribution missed overlapping xs:any wildcards involving
##other: wildcardsOverlap returned false for any pair touching .other, so two
competing wildcards whose namespaces actually intersect were not flagged as
non-deterministic and the schema was wrongly accepted.

Compute the overlap correctly: ##other against ##other always overlaps (both
admit every namespace but their target and the absent one, infinite sets that
always intersect), and ##other against an enumerated set overlaps iff the set
names a namespace that is neither absent (##local) nor the target. The new
predicate matches Wildcard.admits exactly, so disjoint cases (##other vs
##local, or vs the target namespace) stay deterministic and no valid schema
is newly rejected.

Verified against the full W3C XSTS 2006-11-06 suite: invalid-schemas-accepted
35 -> 31 (wildI009/wildI013/wildI014 and one more), valid-schemas-rejected
held at 0 (no other bucket moved). Adds UPA unit tests for both rejection
shapes and a ##other-vs-##local determinism guard; updates the pinned XSTS
count 35 -> 31.
2026-06-21 21:40:56 +02:00
96ab396eb3 feat(schema): locate names/UPA/notation-pass schema diagnostics
Convert the three names/UPA/notation-pass compile checkers to located
findings so each error attaches to its declaring node instead of the schema
root (#169): component-name uniqueness underlines the second duplicate global,
identity-constraint name uniqueness underlines the second constraint, a
keyref whose refer names no key/unique (or whose arity differs) underlines
the keyref (KeyrefInfo gains the keyref node), a Unique Particle Attribution
violation underlines the ambiguous content-model group, and notation
validity underlines the notation or enumeration.

Every predicate, aggregation order, reason string, and the message-dedup of
the UPA pass are unchanged; only the location each finding carries differs,
verified conformance-neutral by the full XSTS suite (14383 groups, pinned
counts unchanged). The findUniqueIndices circular-import (rootIndex)
workaround is left byte-identical, since it governs which containers are
examined, not where findings locate. The three checkers had exactly one
caller each (their dispatch), so each is renamed in place and the .unlocated
wrap dropped. Adds a located-diagnostics test per sub-check.
2026-06-21 19:16:54 +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