Commit Graph

692 Commits

Author SHA1 Message Date
0ff46f6b78 test(parsing): cover streaming ISO-2022-JP and EUC-TW decode
Exercise the byte-at-a-time ISO-2022-JP mode escapes (ASCII, JIS-Roman,
half-width katakana, JIS X 0208) and EUC-TW plane-1 plus the 0x8E four-byte
selector. Raises streaming ISO-2022-JP coverage from 54% to 92% and EUC-TW
from 52% to 76%. Output is compared by Unicode scalar to avoid a grapheme
cluster swallowing the trailing sound mark.
2026-06-22 17:11:58 +02:00
d15faed122 test(relaxng): cover include and externalRef resolution
Exercise RelaxNGParserIncludes (plain include, define override, externalRef)
through the schema loader; raises its line coverage from 24% to 81%.
2026-06-22 17:01:15 +02:00
3cf44cccb9 test(ci): add an opt-in coverage gate; close the 0% value types it surfaces
The coverage analysis noted there is no mechanism that measures test completeness,
so a source file silently sitting at 0% (untested or dead) is invisible.

Add scripts/check-coverage.sh: it runs the instrumented suite, prints overall
line/region/function coverage and the lowest-covered files, and FAILS if any source
file is at 0% line coverage (the exact signal that found the earlier gaps). It is
opt-in (instrumented build + llvm-cov are slow and macOS-only) and skips cleanly
where xcrun is unavailable; an optional COVERAGE_REGION_FLOOR enforces a floor.

Running it immediately surfaced two more 0% value types, now closed by
CoverageGateValueTypeTests: the HTML tokenizer's `Token` (a custom Equatable) and
the public `Stream.Document` (built by API consumers, never internally). Overall
line coverage is now 92.66% with no source file at 0%, so the gate passes.

Tooling + tests only; +2 tests; full suite green (1570).
2026-06-22 16:38:30 +02:00
bd0b47a535 test(schema,xslt,parsing): standing coverage for the gaps a coverage run found
A code-coverage pass found two source files at 0% in the standing suite (covered
only by opt-in gates) and one large under-covered file:

- XSDParserUnion.swift (Document(composing:), the multi-document union, #161) was
  exercised only by the opt-in XSTS harness, so plain `swift test` never ran
  multi-document composition. SchemaComposingTests now covers it via a
  cross-document substitution group (0% to 92% line).
- XSLTCollation.swift (language-tailored xsl:sort orders) was unexercised by any
  standing test. XSLTCollationTests covers the Polish/Russian tables and the
  fallback (0% to 88% line).
- StreamingDecoder.swift (byte-at-a-time CJK/UTF decode) had the streaming/split
  path under-tested. StreamingDecoderTests feeds bytes one at a time through GBK,
  Big5, Shift_JIS, EUC-KR, EUC-JP, GB18030, the UTF-16/32 BOM paths, and a
  malformed sequence (48% to 80% line, 37% to 72% region). Expected scalars were
  taken from the decoder and pinned.

Test-only; +10 tests; full suite green (1568).
2026-06-22 16:16:32 +02:00
b8c8ed56c7 refactor(schema): split Element-Declarations-Consistent out of SchemaStructure
SchemaStructure.swift was the last source file at the 400-line cap. The
cos-element-consistent check (a complex type's content model may not declare two
same-named elements with different types) is a self-contained constraint with its
own helpers (collectElementDecls, elementTypeKey) and the contentModelContainers
set used only by it.

Move that cluster into SchemaElementDeclarationsConsistent.swift;
elementDeclsConsistentErrors becomes internal (its sole caller is in
SchemaStructure). Pure code move, no behaviour change: SchemaStructure 400 -> 342,
new file 59. With this, no source file remains at the 400-line cap. Full suite and
the XSTS gate pass unchanged.
2026-06-22 15:30:58 +02:00
fc8d8faeeb refactor(schema): split redefine attribute-group restriction into its own file
SchemaRedefineSelfReference.swift had reached the 400-line limit, blocking further
redefine-restriction work without a squeeze. It also mixed three cohesive concerns:
redefine self-reference well-formedness (src-redefine.6/7.2.1), attribute-group
restriction (cos-ct-restricts over the redefinition), and model-group restriction.

Move the attribute-group restriction machinery (redefineAttributeGroupRestrictionFindings
and its helpers) into SchemaRedefineAttributeGroupRestriction.swift, following the
established one-concern-per-file pattern (IdentityFieldType.swift). The shared base-
component lookups (baseComponents/resolveBaseComponent) become internal so both files
use them. Pure code move, no behaviour change: 171 + 232 lines (was 400); full suite
and the XSTS gate pass unchanged (20/0/22/0 buckets, 1558 unit tests).
2026-06-22 15:07:53 +02:00
ca503bdee8 refactor(tests): trim redundant burn-down history from the XSTS gate file
The XSTSSuiteTests doc comment had grown a ~120-line per-step burn-down narrative
that duplicated CHANGELOG.md (every entry already said "per-step deltas in
CHANGELOG.md") and kept the file pinned at the 400-line limit, so each conformance
iteration had to hand-squeeze it to stay under the cap.

Replace the narrative with a concise pointer to CHANGELOG.md and
docs/xsts-deviations.md (their canonical home); keep the baseline explanation, the
run instructions, the pinned-count constants, and the specDivergentInstances
exclusion unchanged. File 400 -> 286 lines; the gate's behaviour and pinned counts
are unchanged (verified: XSTSSuiteTests still passes at 20/0/0/22).
2026-06-22 14:41:12 +02:00
74b9332c48 fix(schema): reject identity field whose declared target is a non-simple complex type
cvc-identity-constraint.3 / c-fields-xpaths require an identity-constraint field to
identify a node with a simple type. The prior fix rejected a field whose selected
instance node carries element children (structural). This adds the case where the
field's DECLARED type is a complex type with non-simple content (empty or
element-only/mixed): an attributes-only element resolves to an empty-content complex
type, so its instance node is childless yet not simple-typed.

The field's target type is resolved through the schema type model (the selector
target's declared type, then the field's same-named child element in its content
model) and classified. A simple or simpleContent target, a type reached only
through a named reference, a complex field path, or an unresolvable target is left
alone, so no valid field is rejected. The resolution helpers live in a separate
extension file to keep the validator within the type-body limit.

This catches W3C XSTS idG006. XSTS invalid-instances-accepted 23 -> 22,
valid-instances-rejected held at 0.
2026-06-22 13:10:30 +02:00
18fd42b726 fix(schema): apply substitution-group block to a member with an inline type
The substitution-group filter drops a member when the head element's
{disallowed substitutions} (or the head type's {prohibited substitutions}) blocks
the method by which the member's type derives from the head's type. It looked the
member's type up in the named derivation backbone, so a member with an INLINE
(anonymous) complex type had no entry and was never dropped, even when its inline
type extended the head's type and the head blocked extension.

Record an inline-typed element's derivation under a synthetic key (the element key
plus a `#` marker no QName can contain, so it cannot collide with a real type and
is unreachable as any other type's derivation base) so the filter can see the
member's method. A member reaching the head's type by an unblocked method, a plain
inline type with no derivation, and same-local-name elements in different
namespaces are all unaffected.

This catches W3C XSTS disallowedSubst00105m (and a sibling). XSTS
invalid-instances-accepted 25 -> 23, valid-instances-rejected held at 0.
2026-06-22 06:21:23 +02:00
2419ab43d9 fix(schema): reject identity-constraint field that identifies a complex-content node
Per cvc-identity-constraint.3 / c-fields-xpaths an identity-constraint
(key/unique/keyref) field must evaluate to a node with a simple type: a
simple-typed element, an element with simple content, or an attribute. The
field-cardinality clause (at most one node) was enforced, but the simple-content
clause was not, so a field selecting a complex-content element was wrongly
accepted at instance-validation time.

Add a structural, false-positive-free check: a field whose selected node is an
element carrying element children has complex (element-only or mixed) content and
can never be simple-typed, so it is flagged. A simple-typed or simple-content
element (text only), an empty or text-only node, and an attribute are left alone,
so no valid field is rejected.

This catches W3C XSTS idK012. XSTS invalid-instances-accepted 26 -> 25,
valid-instances-rejected held at 0.
2026-06-22 04:53:58 +02:00
41b3a707c3 fix(schema): flatten redefine self-reference and exclude prohibited uses in attribute uniqueness
The attribute-use uniqueness check (ag-props-correct.2 / ct-props-correct.4)
flattens a complex type's or attribute group's uses through attributeGroup
references. A reference to a redefined group whose redefinition self-references its
former self stopped at the cycle guard, so the original's attributes were never
reached and a duplicate they cause was missed.

Expand a redefinition's self-reference to the group's pre-redefine base definition
(already retained in the context for this purpose) so the original's attributes
join the set; a genuine non-redefine cycle still terminates without contributing.

Separately, exclude use="prohibited" attributes from the duplicate-name and
single-ID counts: a prohibited use is a removal marker, not a member of
{attribute uses} (XSD 3.4.2 / 3.6.2). This is the spec-correct reading and keeps a
self-reference that re-imports an attribute and then prohibits it from being
counted as a duplicate.

This catches W3C XSTS attQ011 and attQ017. XSTS schema invalid-accepted 22 -> 20,
valid-rejected held at 0.
2026-06-22 03:52:13 +02:00
64e30fe491 fix(schema): reject redefined attribute group that self-references and re-declares an attribute
A redefinition of an attributeGroup that self-references the original re-imports
all of its attribute uses, so directly re-declaring one of those attributes
produces two attribute uses of the same name, a duplicate forbidden by
ag-props-correct.2.

Flag a non-prohibited direct re-declaration of an attribute the original directly
declares, when the redefinition has a self-reference. To stay false-positive-free
the check fires only when both the original and the re-declaration land in no
namespace (a qualified attribute's target namespace depends on cross-document
form/chameleon resolution, so it is not compared); a self-referencing redefinition
that adds genuinely new attributes (attgC007/attgC038/groupB018) stays valid.

Also extracts the use-transition restriction errors into a helper to keep the
function within the complexity limit.

This catches W3C XSTS schM8. XSTS schema invalid-accepted 23 -> 22, valid-rejected
held at 0.
2026-06-22 03:07:35 +02:00
b6e5cd8f3f fix(schema): reject redefined attribute group that widens a re-declared attribute's type
A redefinition of an attributeGroup is a restriction of the original
(src-redefine / cos-ct-restricts), so a re-declared attribute's type must derive
by restriction from the original's. The attribute-restriction checks covered
drop-required, re-introduce-prohibited, add-attribute, and relax-fixed, but not a
type change that widens rather than restricts.

Add a built-in-only, namespace-gated check: it fires only when both the original
and the re-declared attribute name a built-in type resolving to the XSD namespace
and the re-declared built-in does not derive from the original's in the XSD
derivation lattice (int does not derive from boolean). A user, inline, or absent
type on either side is not compared, so the check declines and stays lenient: an
absent original type is anySimpleType (which any type restricts), a user/inline
type's derivation is not resolvable here, and a user type whose local name
coincides with a built-in resolves to a different namespace and is not mistaken
for the built-in.

This catches W3C XSTS schM4 (a redefinition re-typing an attribute from boolean
to int). XSTS schema invalid-accepted 24 -> 23, valid-rejected held at 0.
2026-06-22 02:38:24 +02:00
81805ad53e fix(schema): reject redefined attribute group adding attributes via non-self reference
A redefinition of an attributeGroup is a restriction of the original
(src-redefine / cos-ct-restricts), so it may not introduce an attribute the
original lacks unless the original carries an attribute wildcard. The existing
add-attribute check resolved only the redefinition's DIRECT declarations and was
suppressed whenever the redefinition carried any attributeGroup reference, which
conflated a self-reference (re-importing the original's own attributes, valid)
with a non-self reference (injecting foreign attributes, invalid).

Resolve the attribute names a non-self reference contributes and key the
suppression on a genuine self-reference (matched by namespace and local name).
The resolution declines, staying lenient, whenever the contributed set is not
fully knowable: an unresolved group, an attribute-by-ref, an attribute wildcard,
or a further nested reference. This catches W3C XSTS attgC028 (a redefinition
whose ref="car" replaces the original's attributes with foreign ones) while
keeping attgC007/attgC038/groupB018 (added attributes alongside a self-reference)
valid.

XSTS schema invalid-accepted 25 -> 24, valid-rejected held at 0.
2026-06-22 02:06:57 +02:00
6e7f1b7bfc fix(schema): an unprefixed type ref to a target-namespace type is undeclared
isUndeclaredReferenceType matched a no-namespace type/base/itemType/memberTypes
reference by local name, ignoring that an unprefixed QName with no default
xmlns resolves to the absent namespace: type="t" is {}t, not {urn:x}t where the
type is declared, so the reference does not resolve (XSD 1.0 §3.15.3). It now
mirrors isUndeclaredReferenceRef's nil-namespace branch: a no-namespace
reference is undeclared when the schema has a non-empty target namespace; a
chameleon-included global and an imported no-namespace type (via the foreign
pools, checked first) remain accepted.

This corrects an asymmetry where the ref side already rejected the pattern but
the type side did not. The existing SchemaXSIAttributeTests case that asserted
this very pattern compiles used an invalid schema (the same shape as XSTS
addB009/test66059); its type reference is now qualified (tns:attRef), which
preserves the test's xsi:Not-Allowed intent while being valid.

Verified against the full W3C XSTS 2006-11-06 suite: invalid-schemas-accepted
27 -> 25 (addB009/test66059 and one more), valid-schemas-rejected held at 0,
no other bucket moved. Adds a reference test for the type-side rule.
2026-06-21 23:17:14 +02:00
7efc64479b fix(schema): a ref resolving to the XSD namespace is undeclared
isUndeclaredReferenceRef fell through to a lenient accept when a ref or
substitutionGroup resolved to the XSD namespace but the schema did not target
it. The XSD namespace declares no referenceable user component (element,
group, attribute, attribute group), so such a reference is undeclared, the
asymmetry isUndeclaredReferenceType already handled for type/base.

This is reached when an unprefixed reference picks up a default xmlns of the
XSD namespace, so it is not in the target namespace where the schema's own
components live: xmlns="http://www.w3.org/2001/XMLSchema" targetNamespace="foo"
with <element ref="root"/> resolves to {XSD}root, not {foo}root (XSTS
xsd013/xsd014). A schema that itself targets the XSD namespace is the
inTargetNamespace case, and a genuinely loaded XSD-namespace document is the
foreignPools case before this branch, so both are unaffected.

Verified against the full W3C XSTS 2006-11-06 suite: invalid-schemas-accepted
29 -> 27, valid-schemas-rejected held at 0, no other bucket moved. Adds
reference tests for both reject cases and a qualified-ref guard.
2026-06-21 22:57:20 +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
97c07e91f4 feat(schema): locate composition/redefine-pass schema diagnostics
Convert the five composition/redefine compile checkers to located findings,
the last of the #169 burndown (#210-#217): a resolved schemaLocation whose
content is not a schema underlines the include/import/redefine reference, a
redefined type that does not restrict or extend itself underlines the
redefined type, a redefined group or attribute group with a malformed
self-reference underlines the offending reference or redefinition, and a
redefined attribute group or model group that does not restrict its original
underlines the redefinition.

To locate the referenced-schema rule, failedSchemaReferences carries the
referencing node, captured at the load-failure site in XSDContext. Only a
top-level reference's node belongs to the main schema tree the diagnostic
resolves against; a failure nested in a loaded sub-document is captured from
a throwaway parse, so it stays unlocated (the prior schema-root behavior)
rather than mis-resolve, a strict improvement.

Every predicate, aggregation order, and reason string is unchanged; only the
location each finding carries differs, verified conformance-neutral by the
full XSTS suite (14383 groups, pinned counts unchanged). The redefine
checkers had exactly one caller each, so each is renamed in place and the
.unlocated wrap dropped. Adds a located-diagnostics test per node-locatable
checker; the attribute-group and model-group restriction redefine checkers
share the same redefinition-node idiom and are exercised by the XSTS suite.
2026-06-21 19:41:31 +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
c6dd0f7d2f feat(schema): locate extension/substitution-pass schema diagnostics
Convert the three extension/substitution-pass compile checkers to located
findings so each error attaches to its declaring node instead of the schema
root (#169): an xs:all group reached through an extension over a base that
already has content underlines the extension (extensionAllGroupFindings); an
anonymous complex-type restriction that is not a subset of its base
underlines the restriction (anonymousRestrictionFindings); and a
substitution-group member whose type is not validly derived from its head,
including the head's final-exclusion violations, underlines the member
element (substitutionTypeFindings).

Every predicate, iteration order, and reason string is unchanged; only the
location each finding carries differs, verified conformance-neutral by the
full XSTS suite (14383 groups, pinned counts unchanged). The
substitution checker's checkExclusions helper stays [String] and is wrapped
onto the member element at the call site. 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 checker.
2026-06-21 18:35:38 +02:00
4dd1d330cb feat(schema): locate reference-resolution schema diagnostics
Close the last superficially-located checker (#169): referenceFindings was
dispatched as located but its body wrapped the whole aggregate in
.unlocated, so every reference-resolution error still collapsed onto the
schema root. Convert all six sub-collectors to located findings, each
attaching to the node in scope: the core type/base/itemType/memberTypes/ref/
substitutionGroup walk and the xs:-namespace builtin check locate on the
referencing element; the simpleContent base-kind check on the restriction;
the un-imported-namespace check on the referencing element; the
redefine-component-existence check on the redefine's child component; and
the import-namespace-agreement check on the import element.

Every predicate, aggregation order, and reason string is unchanged; only the
location each finding carries differs, verified conformance-neutral by the
full XSTS suite (14383 groups, pinned counts unchanged). Removes two dead
helpers exposed along the way: the unused referenceErrors [String] wrapper
and the unused locatedFindings helper in SchemaCompileValidation. The outer
aggregation is rewritten from a single + chain to incremental appends to keep
the type-checker within budget. Adds a located-diagnostics test per
node-locatable sub-collector; the loader-dependent redefine/import families
are exercised by the XSTS suite.
2026-06-21 18:09:20 +02:00
2b465e9c9c feat(schema): locate content-derivation-pass schema diagnostics
Convert the seven checkers the complexExtensionBaseValid compile rule
dispatches (the cos-ct-extends/src-ct content-derivation family) to located
findings so each error attaches to its derivation node instead of the schema
root (#169): a complexContent extension adding element content over a
simpleContent base, a simpleContent restriction whose base is a built-in or
user simple type, an unfaithful inline type in a simpleContent restriction, a
complexContent base that is a simple type, a simpleContent extension whose
base is xs:anyType, and a mismatched mixed setting on a complexContent
extension each underline their extension/restriction node; an element
carrying a default/fixed value over element-only or empty content underlines
the element.

Each predicate, iteration order, and reason string is unchanged; only the
location each finding carries differs. The seven checkers had exactly one
caller each (the dispatch), so each is renamed in place and the .unlocated
wrap dropped. To keep the file under the 400-line limit, the seven functions
and their two private helpers move to a new SchemaContentDerivation.swift;
the shared extensionAddsElementContent helper, still used by the all-group
checker that stays behind, becomes module-internal. Adds a located-
diagnostics test per checker.
2026-06-21 17:29:41 +02:00
ee5c46929c feat(schema): locate derivation/cycle-pass schema diagnostics
Convert the three derivation/cycle-pass compile checkers to located findings
so each error attaches to its declaring node instead of the schema root
(#169): an all-group referenced inside a sequence/choice/all locates on the
offending group reference (allGroupReferencePlacementFindings); a type
derived from itself locates on its complexType/simpleType
(derivationCycleFindings); and an element that is a member of its own
substitution group, or a model group or attribute group that references
itself, locates on its declaration (circularReferenceFindings).

The cycle checkers report a cycle by component name over a name-keyed graph,
so each now records the declaring node in a parallel nodeOf map built in the
same loop as the graph; a name that yields a finding always has a node, so a
mutual cycle underlines each member on its own declaration. Every predicate,
key ordering, and reason string is unchanged; only the location each finding
carries differs. The three checkers had exactly one caller each, so each is
renamed in place and the .unlocated wrap dropped. Adds a located-diagnostics
test per checker family, covering both substitution and group-reference
cycles.
2026-06-21 16:53:02 +02:00
49fe824601 feat(schema): locate attribute-pass schema diagnostics
Convert the three attribute-pass compile checkers to located findings so
each error attaches to its declaring node instead of the schema root (#169):
an attribute declaration in the XSI namespace locates on the attribute
(xsiNamespaceAttributeFindings); a duplicate attribute use or a second
ID-typed attribute locates on its declaring complexType or attributeGroup,
and an attribute reference contradicting a global fixed value locates on the
reference (attributeUseFindings); and a complex-type restriction that
relaxes a base attribute use or widens the attribute wildcard locates on the
restriction (attributeRestrictionFindings).

Each predicate, traversal order, and reason string is unchanged; only the
location each finding carries differs. The set-level uniqueness/single-ID
findings reuse the existing attributeSetErrors helper, mapping each onto its
declaring container node. An attribute restriction violation has no source
node of its own (the attribute use is a resolved model value), so it locates
on the restriction performing the illegal narrowing. The three checkers had
exactly one caller each, so each is renamed in place and the .unlocated wrap
dropped. Adds a located-diagnostics test per checker family.
2026-06-21 16:28:10 +02:00
356e87581c feat(schema): locate simple-type-pass schema diagnostics
Convert the four simple-type-pass compile checkers to located findings so
each error attaches to its own declaring node instead of the schema root
(#169): a list whose item type is itself a list or a union containing a
list and a list declaring both or neither of itemType/inline simpleType
(simpleTypeVarietyFindings), an empty union, a simpleType restriction whose
base resolves to a complex type (simpleTypeBaseNotComplexFindings), an
itemType/memberType referencing a type final for list/union derivation
(simpleTypeFinalFindings), and an attribute typed by a complex type
(attributeTypeMustBeSimpleFindings).

Each predicate, traversal order, and reason string is unchanged; only the
location each finding carries differs, from the schema root to the
offending list, union, restriction, or attribute node. The four checkers
had exactly one caller each (their compile-rule dispatch), so each is
renamed in place and the .unlocated wrap dropped, leaving no dead wrapper.
Adds a located-diagnostics test per checker in a new suite kept within the
type-body budget.
2026-06-21 15:55:59 +02:00
3a56d9f400 feat(schema): locate structure-pass schema diagnostics
Convert the seven structure-pass compile checkers to located findings so
each error attaches to its own declaring node instead of the schema root
(#169): a facet inapplicable to a list/union variety, a restriction of
xs:anySimpleType and a constraining facet on anySimpleType-based simple
content, a top-level definition missing its name or carrying a local-only
attribute, a nested named definition, an empty targetNamespace or import
namespace, and an all-group reference with an illegal minOccurs/maxOccurs.

The rewire in structureFindings preserves the prior finding order and
count; each error predicate and reason string is unchanged, only the
location each finding carries differs. Adds a located-diagnostics test
per checker family in a new suite kept within the type-body budget.
2026-06-21 15:23:13 +02:00
d53cdaad1c docs(changelog): note located value-constraint diagnostics (#169) 2026-06-21 14:37:29 +02:00
76ef2d5b83 feat(schema): locate value-constraint type errors
Convert the three value-constraint checkers (built-in, named user type,
and inline type) to located findings so a default/fixed value that is
invalid for its declared type, or an illegal combination of value
constraints, is attached to its declaring element or attribute instead
of the schema root. An editor can underline the offending declaration
by line and column.

Error predicates and reason strings are unchanged; only the location
each finding carries differs. Adds located-diagnostics tests for the
built-in, user-type, and inline-type paths.
2026-06-21 14:37:29 +02:00
f5059de42e feat(schema): locate ID-typed value-constraint errors
Convert idValueConstraintErrors to idValueConstraintFindings so each
a-props-correct.3 / e-props-correct.5 violation is attached to its
declaring element or attribute node, letting an editor underline the
offending declaration by line and column instead of the schema root.

The error predicate and reason strings are unchanged: only where each
error points differs. Adds a located-diagnostics test asserting the
finding resolves to the declaring component's source range.
2026-06-21 14:37:29 +02:00
fa3552876a fix(schema): saturate effectively-unbounded content-model counters (#192, partial)
The counted content-model automaton represents its active set as
(state, counter-vector) configurations bounded by states times the product of
the per-counter saturation caps. An unbounded counter already saturates at its
minimum (every higher count is matching-equivalent). A finite maximum the input
cannot reach is equivalent: the active set increments a counter at most to its
cap, the cap is at most inputLength+1, so when maximum > inputLength+1 the value
can never reach maximum, belowMaximum is always satisfied, and every value at or
above the minimum is indistinguishable. Saturate such a counter at its minimum
too, so a huge but finite maxOccurs on a nested counter no longer multiplies the
active set by an inputLength factor per nesting level.

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

Partial: a model with several moderate (input-reachable) maxOccurs nested deeply
still enumerates a large active set; the general polynomial bound (a position-
indexed DP over the counting automaton) remains under #192.
2026-06-21 13:16:24 +02:00
7a126a9b0a test(schema): enforce streaming-vs-tree agreement as a standing gate
The streaming XSD validator is a separate implementation of the same semantics
as the tree validator (the conformance oracle), and three false positives in
it were found and fixed this cycle (#186, #200, #202) only by adversarial
hunting, because the XSTS gate exercises the tree path, not validate(streaming:).
A snapshot is not an invariant.

Add test_streamingMatchesTreeDifferential to the generative fuzz suite: 2000
deterministic schema/instance pairs (15 on WASI), asserting that streaming and
the tree validator agree on whether each document is valid (so streaming never
rejects a valid document and never accepts an invalid one the tree rejects),
and that the tree's errors are a subset of streaming's (streaming may add
errors on an already-invalid document, #206, but may never miss a real error
or invent a rejection). A failure prints the seed, schema, instance, and both
error sets, so it reproduces exactly.

This runs in the standard suite on all five platforms, so a future streaming
false positive fails CI rather than reaching users.
2026-06-20 19:54:05 +02:00
ca7a7d1433 fix(schema): a reference to an all group must have maxOccurs 1
XSD 1.0 cos-all-limited: a reference to a model group whose content is an
`all` group is itself an all-group particle, so its maxOccurs must be 1 and
its minOccurs 0 or 1. The direct `<all>` element was already checked by
allGroupLimitedErrors, but a `<group ref>` resolving to an all group was not,
so corpus particlesEa025 (`<group ref="x:G" maxOccurs="2">` to an all group)
was accepted.

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

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

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

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

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

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

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

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

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

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

Streaming now agrees with the tree-path oracle across these, verified by an
adversarial streaming-vs-tree differential. No XSTS bucket changes (the tree
path is the conformance oracle); gate held at 0/37/0/26.
2026-06-20 16:38:56 +02:00
8bb7568f6e test(schema): pin restriction mixed-content asymmetry
XSD cos-ct-restricts restriction mixedness is one-directional: a restriction
may keep mixedness or narrow mixed to element-only, but must not add mixed
content to an element-only base. The two #184 targets are already rejected
(ctZ010d by extensionMixedAgreementErrors, ctZ010e by ParticleRestriction's
mixed-vs-element-only case) and the four guard schemas already compile, so no
behavior change is needed. This adds the isolated regression pin the
restriction half lacked, guarding against a bidirectional "derived mixed must
equal base mixed" rule, which regressed four valid schemas when last tried.
2026-06-20 15:03:51 +02:00
d8794579db fix(schema): forbid attribute declarations in the XSI namespace
XSD 1.0 Schema Component Constraint "xsi: Not Allowed" (§3.2.6): an attribute
declaration's {target namespace} must not be the XSI namespace. A schema may
target XSI and declare unqualified attributes (they land in no namespace),
but a top-level attribute, or a local one made qualified by form="qualified"
or attributeFormDefault="qualified", lands in XSI and is forbidden.

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

XSTS invalid-schemas-accepted 39 -> 37; valid-schemas-rejected held at 0; no
other bucket moved.
2026-06-20 14:33:14 +02:00
4735acbf03 docs(roadmap): add epic #195 and sync XSTS counts
Add the #195 xsi:type/substitution derivation epic to the README roadmap
diagram (the epic-coverage gate requires every epic issue to appear), and
update the settled XSTS counts to 0/39/0/26.
2026-06-20 13:50:55 +02:00
7256df1ceb fix(schema): reject references into never-imported namespaces
On the cross-document skip path (a schema declares import/include/redefine
but no external document was loaded), the reference check stood down whole,
so a prefixed reference whose namespace is never imported was accepted even
though it can resolve to no component under any loading (corpus schZ011's
d:d, where namespaces a/b/c are imported but d is not).

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

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

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

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

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

Closes the XSTS dateTime011 false negative: invalid-instances-accepted 31 -> 30,
no other bucket moved (valid-schemas-rejected and valid-instances-rejected held at
0). 1449 unit tests pass; the full XSTS suite settles at 0/43/0/30.
2026-06-20 01:31:14 +02:00
d9ebfc1998 docs: correct the last stale valid-schemas-rejected figure
The Antigravity onboarding definition-of-done checklist still read
'valid-schemas-rejected still 1'; the enforced baseline is 0
(Tests/XSTSSuiteTests.swift), matching the rest of this file and GEMINI.md.
Caught by the PR review pass.
2026-06-19 23:32:04 +02:00
cd6201ca5c fix(schema): make the container-location root fallback live
containerLocationMap stored `loc ?? rootLocation` into a [ObjectIdentifier:
String?]. Because the value type is itself optional the coalescing bound at the
outer optional and rootLocation was never used (the compiler flagged it), so a
container with no location of its own (the compile root) mapped to nil even when
a circular import had loaded the root under a schemaLocation. An explicit String?
annotation keeps the coalescing at the String level so the root inherits
rootLocation.

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

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

particlesQ005 goes from a multi-minute near-hang to about two seconds with
identical results. The 1448 unit tests pass and the full XSTS suite still
settles at 0/43/0/31, and now completes instead of hanging.
2026-06-19 23:32:04 +02:00
8d30010e0d docs: align stale figures with code and the enforced XSTS gate
- README, roadmap: XSTS status quad and gates table now read the enforced
  0/43/0/31 (Tests/XSTSSuiteTests.swift); test count 986/169 to 1448/258;
  encoding list and the remaining-work section reflect shipped reality;
  check-all expansion lists the validation and changelog gates
- GEMINI.md, onboarding-antigravity.md: valid-schemas-rejected 1 to 0 and the
  pinned baselines updated to current
- SECURITY.md: complete the reporting address
- Parsing.InputEncoding: doc comment lists the full shipped encoding set
- Schema.XSDParserUnion: guard the localName interpolation with ?? "" to match
  the same line and silence an optional-interpolation warning (internal dedup
  key, behavior preserving)
- CHANGELOG: Unreleased entries for the above
2026-06-19 23:32:04 +02:00
b73384bc2e fix(schema): count content-model occurrences 2026-06-19 23:32:04 +02:00