diff --git a/CHANGELOG.md b/CHANGELOG.md index ab029fa..fef28b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- XSTS invalid-schemas-accepted (#145): substitution-group closure now stops + chaining through an intermediate member whose own `block` contains + `substitution`. The blocked element itself may still substitute for its direct + ancestor, but its descendants are no longer added to higher ancestor heads, so + elemZ027_c (a restriction through `a -> b(block=substitution) -> c -> d`) is + rejected while the valid sibling where the blocked member itself is used + remains accepted. invalid-schemas-accepted 45 -> 44, no false positive + (valid-schemas-rejected and valid-instances-rejected held at 0), no other + bucket moved. + - XSTS invalid-schemas-accepted (#145): a substitution-group head typed `xs:anySimpleType` now admits simple type members and complex types with `simpleContent`, but rejects element-only, empty, and mixed complex-type members. The shared derivation oracle treats `anySimpleType` as a broad ur-type for other restriction checks, so the substitution-group rule now adds its own value-space guard before accepting the affiliation. stZ048 is rejected while the valid simple/simpleContent near-misses remain accepted. invalid-schemas-accepted 46 → 45, no false positive (valid-schemas-rejected and valid-instances-rejected held at 0), no other bucket moved. - XSTS invalid-schemas-accepted (#145): a `complexContent` extension that explicitly changes mixedness, or that adds element content while defaulting to `mixed="false"`, must match the base type's mixed setting. This rejects ctZ010d (mixed base extended with new element content by an omitted/default-false derived type) while preserving the valid ctZ012b near-miss (empty extension inherits the mixed base) and the invalid ctF008 explicit mixed mismatch. invalid-schemas-accepted 47 → 46, no false positive (valid-schemas-rejected and valid-instances-rejected held at 0), no other bucket moved. diff --git a/Sources/Schema/XSDParser.swift b/Sources/Schema/XSDParser.swift index 231ebae..3f76307 100644 --- a/Sources/Schema/XSDParser.swift +++ b/Sources/Schema/XSDParser.swift @@ -84,7 +84,14 @@ extension PureXML.Schema.XSDParser { groups: indexByName(allChildren(containers, named: "group")), targetNamespace: mainTargetNamespace, substitutions: filterSubstitutions( - XSDNode.substitutionMembers(containers, namespaceMap: namespaceMap, mainTargetNamespace: mainTargetNamespace), + XSDNode.substitutionMembers( + containers, + namespaceMap: namespaceMap, + mainTargetNamespace: mainTargetNamespace, + chainBlockedBySubstitution: Set(derivation.nsElementBlock.compactMap { element, block in + block.contains(.substitution) ? element : nil + }), + ), derivation, ), abstractElements: derivation.abstractElements, diff --git a/Sources/Schema/XSDSubstitutionMembers.swift b/Sources/Schema/XSDSubstitutionMembers.swift index c3928db..0099969 100644 --- a/Sources/Schema/XSDSubstitutionMembers.swift +++ b/Sources/Schema/XSDSubstitutionMembers.swift @@ -4,11 +4,14 @@ extension PureXML.Schema.XSDNode { /// it, directly or through a chain of heads. Keyed by namespaced identity /// (`{ns}local`), so a head and a member with the same local name in different /// namespaces stay distinct. `namespaceMap` gives each container's resolved - /// target namespace by index. + /// target namespace by index. A member whose own `block` contains + /// `substitution` remains a member of its direct head but is not chained + /// through to add its own members to an ancestor head. static func substitutionMembers( _ containers: [XSDTree], namespaceMap: [Int: String?], mainTargetNamespace: String?, + chainBlockedBySubstitution: Set, ) -> [String: [String]] { var direct: [String: [String]] = [:] for index in containers.indices { @@ -41,6 +44,7 @@ extension PureXML.Schema.XSDNode { index += 1 guard seen.insert(member).inserted else { continue } members.append(member) + guard !chainBlockedBySubstitution.contains(member) else { continue } queue += direct[member] ?? [] } closure[head] = members diff --git a/Tests/SchemaSubstitutionBlockTests.swift b/Tests/SchemaSubstitutionBlockTests.swift index 28d079a..bab6be4 100644 --- a/Tests/SchemaSubstitutionBlockTests.swift +++ b/Tests/SchemaSubstitutionBlockTests.swift @@ -56,6 +56,38 @@ struct SchemaSubstitutionBlockTests { #expect(try !schema.validate(#""#).isEmpty) } + @Test("an intermediate substitution block stops descendants from restricting an ancestor head") + func test_substitutionBlockedHeadDoesNotChainDescendants() { + #expect((try? PureXML.Schema.Document(""" + + + + + + + + + + + """)) == nil) + } + + @Test("a substitution-blocked member can still restrict its own ancestor head") + func test_substitutionBlockedHeadItselfStillRestrictsAncestor() { + #expect((try? PureXML.Schema.Document(""" + + + + + + + + + + + """)) != nil) + } + /// cvc-elt.4.3.2.1: an xsi:type naming a complex type must be derived from the /// element's declared type. A complex type on a different branch of the /// hierarchy is rejected; the declared type itself is valid. diff --git a/Tests/XSTSSuiteTests.swift b/Tests/XSTSSuiteTests.swift index e57cb53..3546789 100644 --- a/Tests/XSTSSuiteTests.swift +++ b/Tests/XSTSSuiteTests.swift @@ -147,7 +147,7 @@ struct XSTSSuiteTests { /// attribute/simple-type rules, composition, simpleContent inline-type checks, /// complexContent mixedness, and substitution-group anySimpleType checks. private let knownSchemaValidRejected = 0 - private let knownSchemaInvalidAccepted = 45 + private let knownSchemaInvalidAccepted = 44 private let knownInstanceValidRejected = 0 /// Instance bucket (133 -> 31), per-step deltas in CHANGELOG.md: xsi:type must derive from the /// declared type (complex, then atomic and list/union, resolved through the element-ref chain); diff --git a/docs/plans/2026-06-18-false-negative-tail.md b/docs/plans/2026-06-18-false-negative-tail.md index db1cc4b..abc6234 100644 --- a/docs/plans/2026-06-18-false-negative-tail.md +++ b/docs/plans/2026-06-18-false-negative-tail.md @@ -8,7 +8,7 @@ ratchet one clause at a time. ## Current measurement -Measured from `/tmp/xsts-failures.txt`, last written 2026-06-18 22:59 local time +Measured from `/tmp/xsts-failures.txt`, last written 2026-06-19 08:20 local time by `XSTSSuiteTests`. ```sh @@ -20,7 +20,7 @@ Result: | Bucket | Count | |---|---:| | valid schema rejected | 0 | -| invalid schema accepted | 45 | +| invalid schema accepted | 44 | | valid instance rejected | 0 | | invalid instance accepted | 31 | @@ -30,8 +30,10 @@ every task below must keep them at zero. Progress since the starting 48/31 snapshot: `particlesZ018` is closed by the simpleContent inline type-restriction rule, and `ctZ010d` is closed by the complexContent mixed-agreement rule. `stZ048` is closed by the substitution-head -`xs:anySimpleType` member content check. The schema false-negative bucket is down -by three with both false-positive buckets still at zero. +`xs:anySimpleType` member content check. `elemZ027_c` is closed by making +substitution-group closure stop chaining through members whose own `block` +contains `substitution`. The schema false-negative bucket is down by four with +both false-positive buckets still at zero. ## File map @@ -65,7 +67,7 @@ not a proof. Before code, read the XSTS fixture, the matching PureXMLResearch reference, and the XSD 1.0 rule text; update the lane if the evidence says this classification is wrong. -### Invalid schemas accepted: 45 +### Invalid schemas accepted: 44 | Lane | Cases | First owning surface | |---|---|---| @@ -74,7 +76,7 @@ classification is wrong. | Attribute group restriction triage | `attgC028` | `SchemaAttributeRestriction`, redefine machinery | | Attribute type/use triage | `attKa015`, `attKb018a` | `SchemaAttributeApplicability`, `SchemaAttributeRestriction` | | Attribute identity/composition triage | `attQ011`, `attQ016`, `attQ017`, `attQ018` | Attribute restrictions and per-document composition | -| Substitution-group head/member modeling | `elemZ026`, `elemZ027_c`, `elemZ028e` | `SchemaSubstitutionType`, `XSDSubstitutionMembers` | +| Substitution-group head/member modeling | `elemZ026`, `elemZ028e` | `SchemaSubstitutionType`, `XSDSubstitutionMembers` | | ParticleRestriction exactness | `particlesEa025`, `particlesFb003`, `particlesHa161`, `particlesHb011`, `particlesIg004`, `particlesIj008`, `particlesIk011`, `particlesIk025`, `particlesIk027`, `particlesK006`, `particlesM034`, `particlesV020` | `ParticleRestriction*` | | UPA / wildcard determinism | `particlesZ022`, `particlesZ030_d`, `particlesZ039` | `ContentModelDeterminism` | | Regex disclosed-vs-fixable triage | `reK88`, `RegexTest_993`, `RegexTest_1477` | `Sources/Regex/*`, `SimpleType` | @@ -168,7 +170,7 @@ XSTS_ROOT=/private/tmp/xsts/xmlschema2006-11-06 swift test -c release --filter X `Tests/SchemaSubstitutionTypeTests.swift`, `Tests/SchemaSubstitutionBlockTests.swift` -**Does:** Model the `elemZ026`/`elemZ027_c`/`elemZ028e` schema failures and +**Does:** Model the remaining `elemZ026`/`elemZ028e` schema failures and the `disallowedsubst*` instance failures without changing the general MapAndSum/RecurseAsIfGroup logic. The key safety check is that a substitution head/member graph must be namespace-exact and must not conflate same-local names. diff --git a/docs/release-hurdles.md b/docs/release-hurdles.md index 1de786c..db3103b 100644 --- a/docs/release-hurdles.md +++ b/docs/release-hurdles.md @@ -6,7 +6,7 @@ evidence, what actually stands between the current engine and a 1.0 fit to put i front of a developer as the authority on XML/XSD correctness. Snapshot at time of writing: v0.2.0 released; full 2006-11-06 XSTS archive (14383 -groups) with `valid-schemas-rejected = 0`, `invalid-schemas-accepted = 45`, +groups) with `valid-schemas-rejected = 0`, `invalid-schemas-accepted = 44`, `valid-instances-rejected = 0`, `invalid-instances-accepted = 31`. ## Status against the four stoppers @@ -16,7 +16,7 @@ groups) with `valid-schemas-rejected = 0`, `invalid-schemas-accepted = 45`, libxml2 differential over 9618 schemas. **Met.** This is the doc's bar to be "an authority"; it is cleared on the measured corpora. 2. **Silently accepting faulty input (false negatives).** `invalid-schemas-accepted - = 45`, `invalid-instances-accepted = 31`. **Not at 0.** This is the bulk of named + = 44`, `invalid-instances-accepted = 31`. **Not at 0.** This is the bulk of named milestone work (M2, M3) and the subject of Part 1 below. 3. **Uncharacterized correctness.** Differential harness vs libxml2 and a fuzz suite exist (M1, ~done). Gap: real-world corpora beyond XSTS, and the silent @@ -28,7 +28,7 @@ groups) with `valid-schemas-rejected = 0`, `invalid-schemas-accepted = 45`, ## The biggest hurdle, stated plainly -Driving the false-negative buckets (`invalid-schemas-accepted = 45`, +Driving the false-negative buckets (`invalid-schemas-accepted = 44`, `invalid-instances-accepted = 31`) to zero is the largest remaining body of named work, and it is hard for a specific reason: **the ~95% already closed were the tractable cases; every case left fights the cardinal rule.** The remainder sits in @@ -56,7 +56,7 @@ chose to under-approximate to stay false-positive-free. | particlesV020 | MapAndSum (Sequence:Choice) | effective-total-range "approximated by per-particle checks, a documented simplification" (ParticleRestriction.swift:8) | | particlesK006, M034 | RecurseAsIfGroup (Elt:All, Elt:Sequence) | "a documented, bounded under-rejection… over-rejection is the non-starter" (ParticleRestriction.swift:298) | | particlesEa025, Fb003, Ha161, Hb011 | pointless-particle / extension-from-`any` / forbidden sequence:elt | partial | -| elemZ026/027/028 | element-decl restriction where base is a substitution-group head | not modelled | +| elemZ026/028 | element-decl restriction where base is a substitution-group head | partially modelled; elemZ027_c is closed | | particlesZ018 | list-of-int ⊄ decimal (simple-type derivation, a *different* oracle) | closed by inline simpleContent type restriction check | | ctZ010d | mixed base extended with new element content by a default-false derived type (a *different* oracle) | closed by complexContent mixed-agreement check | | particlesZ022/030 | non-deterministic wildcards (UPA, not restriction) | determinism check is deliberately QName-only | diff --git a/docs/schema-validity-burndown.md b/docs/schema-validity-burndown.md index a37439d..3ddcbe2 100644 --- a/docs/schema-validity-burndown.md +++ b/docs/schema-validity-burndown.md @@ -10,7 +10,7 @@ irreducible tail. Do not deviate to new features while this is open. | Bucket | Count | Reading | |---|---|---| | valid-schemas-rejected | **72 -> 0** | production target is 0: rejecting a correct schema is the most user-hostile defect (#148). Particle-restriction over-rejection fixed: pointless `maxOccurs=0` particles removed, content-free restriction of an EMPTY base, empty-derived emptiable-base, and (#163) the pairwise check over the normalized content model with occurrence subsumption applied only to same-kind pairings (cross-kind element/group, group/element, group/wildcard match occurrence inside, by effective total range / member). #164 resolved element-ref namespaces by prefix binding (the `particlesJj/Jk/Jm/Jn/Js`, `particlesQ/R` cluster, -28). Content-free (`maxOccurs=0`) derived groups and substitution-group declaration order closed `particlesW006`/`elemZ027_a`; the W3C MapAndSum rule (Sequence:Choice, #165) closed `particlesV003`. The false-positive schema bucket is now zero. | -| **invalid-schemas-accepted** | **2461 -> 45** | we rarely catch a bad one (facet, default/fixed value, particle-restriction name+type, id, structural content-model, UPA determinism, content order, identity-constraint XPath-subset, wildcard-namespace, final/block, simpleType content, group/wildcard and group/element cardinality, element-ref namespace, sequence/choice MapAndSum, circular type derivation, circular group/attributeGroup/substitution references, attribute-use uniqueness and single-ID, type-excludes-inline-type, ID-typed value-constraint exclusion, all-group reference placement, substitution-member type derivation, default/fixed mutual exclusion, elementTypeRestrictionOK NameAndTypeOK union checks, prohibited attribute type-check bypass, keyref arity verification, top-level group/attributeGroup local-attribute checks, simpleType restriction structure and prefix binding checks, namespace-aware reference resolution checks, simpleType variety constraints on list itemType and union memberTypes, targetNamespace scope matching on local schema references, substitution group exclusions based on element final and schema finalDefault attributes, cross-container component-name and identity-constraint uniqueness checks, notation declarations and notation enumeration restrictions, xml:lang and xml:space on documentation/appinfo elements, duplicate schema container validation across separate files with identical content, schema element order, import constraints validation, facet applicability per base type, fractionDigits constraints on integer types, ordered duration range bounds, built-in list non-emptiness constraints, simpleContent inline type restriction, complexContent mixed-agreement checks, and substitution-head anySimpleType member-content checks landed). | +| **invalid-schemas-accepted** | **2461 -> 44** | we rarely catch a bad one (facet, default/fixed value, particle-restriction name+type, id, structural content-model, UPA determinism, content order, identity-constraint XPath-subset, wildcard-namespace, final/block, simpleType content, group/wildcard and group/element cardinality, element-ref namespace, sequence/choice MapAndSum, circular type derivation, circular group/attributeGroup/substitution references, attribute-use uniqueness and single-ID, type-excludes-inline-type, ID-typed value-constraint exclusion, all-group reference placement, substitution-member type derivation, default/fixed mutual exclusion, elementTypeRestrictionOK NameAndTypeOK union checks, prohibited attribute type-check bypass, keyref arity verification, top-level group/attributeGroup local-attribute checks, simpleType restriction structure and prefix binding checks, namespace-aware reference resolution checks, simpleType variety constraints on list itemType and union memberTypes, targetNamespace scope matching on local schema references, substitution group exclusions based on element final and schema finalDefault attributes, cross-container component-name and identity-constraint uniqueness checks, notation declarations and notation enumeration restrictions, xml:lang and xml:space on documentation/appinfo elements, duplicate schema container validation across separate files with identical content, schema element order, import constraints validation, facet applicability per base type, fractionDigits constraints on integer types, ordered duration range bounds, built-in list non-emptiness constraints, simpleContent inline type restriction, complexContent mixed-agreement checks, substitution-head anySimpleType member-content checks, and substitution-group closure barriers for `block="substitution"` landed). | | valid-instances-rejected | **233 -> 0** | false-positive instance bucket is now zero. | | invalid-instances-accepted | **160 -> 31** | instance side remains open; see the XSTS false-negative tail plan for the current 31-case split. | diff --git a/docs/xsts-deviations.md b/docs/xsts-deviations.md index 8c5b328..774e6d6 100644 --- a/docs/xsts-deviations.md +++ b/docs/xsts-deviations.md @@ -40,8 +40,8 @@ entries removed about 70 apparent deviations that were never bugs. Counts are against *settled* W3C expectations (queried/disputed entries excluded). -Current ratchet (2026-06-18): `valid schema rejected = 0`, -`invalid schema accepted = 45`, `valid instance rejected = 0`, +Current ratchet (2026-06-19): `valid schema rejected = 0`, +`invalid schema accepted = 44`, `valid instance rejected = 0`, `invalid instance accepted = 31`. The historical table above is retained as the published baseline snapshot; the active burn-down is in `docs/plans/2026-06-18-false-negative-tail.md`. @@ -53,13 +53,14 @@ and to document the remainder as spec-justified, deliberate exclusions. ## Category A: genuine correctness bugs -### Fixed (thirty-two root causes, ~2256 deviations cleared) +### Fixed (thirty-three root causes, ~2257 deviations cleared) list-datatype length facets (#146) · union pattern/enumeration facets · duration partial-order facets · QName length non-constraining · the full XSD `\p{Is...}` Unicode block set · element-level `block` against `xsi:type` · empty-element default/fixed values + enumeration-restriction-replaces · `block="substitution"` -on substitution-group heads · `\w`/`\W` XSD definition · document-scoped +on substitution-group heads · substitution-group closure stops chaining through +members whose own `block` contains `substitution` · `\w`/`\W` XSD definition · document-scoped xs:ID/xs:IDREF uniqueness and resolution · `` resolution · mixed-content type with no element model · built-in `xsi:type` resolution gated by substitution validity (the XSD Part 2 derivation lattice), which also closed @@ -72,7 +73,7 @@ in the base space; inclusive/exclusive exclusions; bound ordering) · substitution-group heads typed `xs:anySimpleType` only admitting simple and simpleContent members · `gMonth` `--MM--` lexical form · schema `id` attribute validity (`xs:ID`: NCName and unique within the document). The schema-validity work brought invalid schemas -accepted 2461 to 264 (schema-document structural validity: content model + complexType content shape, attribute values, name/reference + pattern-regex lexical validity, and reference resolution, against the schema-for-schemas, plus simpleType variety constraints on list itemType and union memberTypes, plus targetNamespace scope matching on local schema references, plus substitution group exclusions based on element final and schema finalDefault attributes, duplicate schema container validation across separate files with identical content, schema element order, and import constraints validation); see `schema-validity-burndown.md`. +accepted 2461 to 44 (schema-document structural validity: content model + complexType content shape, attribute values, name/reference + pattern-regex lexical validity, and reference resolution, against the schema-for-schemas, plus simpleType variety constraints on list itemType and union memberTypes, plus targetNamespace scope matching on local schema references, plus substitution group exclusions based on element final and schema finalDefault attributes, duplicate schema container validation across separate files with identical content, schema element order, import constraints validation, and the later tail fixes); see `schema-validity-burndown.md`. Plus the measurement fix (exclude W3C-disputed `status="queried"` entries). ### Remaining, root-caused (the hard tail)