lutaml-xsd 1.2.112 → 1.2.114

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cdfdd257c5d78df578d997d512d39f17f9c83b10d4a8928708767b41bb6224b9
4
- data.tar.gz: c9a0bc6226dd8346324c44af5f93906f851b477895b145ed86a89cc2a70f8538
3
+ metadata.gz: 36f7c7c0e9d599f7400dac905f372692d9da5d253bd5d67069106d923259a4b3
4
+ data.tar.gz: 6a2b5a3b8f8db1067381b3e8f924da0f06af636a152626795167b97012510e40
5
5
  SHA512:
6
- metadata.gz: f503a443b9e205299c93eadd6c4f878f8fe025a2d0bd0514629fcdd38945f533b948afa656aa3e57bd8b5bfc2ade4213f9c77380d5bcc395c279a23b247c1da6
7
- data.tar.gz: 1f533117654e06d3e2acde45f3c053eb99c9c41030b9a844fa598482372db23e24029fe3eaf7514ce7033c8a4c94b9bcd11b62209d00e7ab0b56ccc9458e6e66
6
+ metadata.gz: c9467308b09e8249e3e5b87a1b1a2d298f95d55ec4bf466a63a0a376752e813f0f9753b821d25c000e3d39a0c65249e675df39ec1496f47ecadde12b5782d28d
7
+ data.tar.gz: f147cc2f3ebb9d6ec7ca62601b72573ccc70433bdd36b50cbe46133e815b052ac5a4fec48153e3e2823ab8c6d1f3955f13f1f1714b761b4bfc1310de174b5c20
data/TODO.conformance.md CHANGED
@@ -120,7 +120,7 @@ pinned commit) is measured via `rake conformance:w3c`:
120
120
 
121
121
  | mode | pass-rate |
122
122
  |---|---|
123
- | pipeline | 4644/4696 measured + 5 budget exclusions (98.96% of measured) |
123
+ | pipeline | 4658/4696 (99.19%) — first measure above 99 |
124
124
 
125
125
  (2026-11-02, bundled-stack measurement with the escape,
126
126
  hint-following, pattern-alternative, identity-crash, engine-set,
@@ -144,7 +144,7 @@ lockfile refresh) → 4411 (93.9%) → 4430 (94.3%) → 4435 (94.4%) →
144
144
  → 4557 (97.0%) → 4565 (97.2%) → 4569 (97.3%) → 4562 (drift raw) →
145
145
  4563 (97.17%, ref-namespace round) → 4573 (97.38%, group-content
146
146
  round) → 4578 (97.49%, attribute-prohibition round) → 4585 (97.64%,
147
- multi-document round) → 4588 (97.7%, type-binding round) → 4592 (97.79%, model-161 ride) → 4611 (98.19%, engine-406 ride) → 4613 (98.23%, temporal-enumeration round) → 4614 (98.25%, simple-type-children round) → 4617 (98.32%, ic-ancestor-scope round) → 4637 (parent-context rounds + chameleon round 1.2.109) → 4637 (nested-type-candidates round, 1.2.110) → 4637 (redefine-cycle round, 1.2.111) → 4644 measured (98.96% of measured, train-adoption round: moxml 0.6.6 + leptris 1.9.418.0), +683 net over the 1.2.43 baseline. The escapes round (PR #197 — \d→\p{Nd},
147
+ multi-document round) → 4588 (97.7%, type-binding round) → 4592 (97.79%, model-161 ride) → 4611 (98.19%, engine-406 ride) → 4613 (98.23%, temporal-enumeration round) → 4614 (98.25%, simple-type-children round) → 4617 (98.32%, ic-ancestor-scope round) → 4637 (parent-context rounds + chameleon round 1.2.109) → 4637 (nested-type-candidates round, 1.2.110) → 4637 (redefine-cycle round, 1.2.111) → 4644 measured (98.96% of measured, train-adoption round: moxml 0.6.6 + leptris 1.9.418.0) → 4649 (98.91%, quadratic-dispatch round — first FULL-denominator measure, the 5 reZ budget exclusions retired) → 4658 (99.19%, remainder-classification round — first measure above 99), +697 net over the 1.2.43 baseline. The escapes round (PR #197 — \d→\p{Nd},
148
148
  \w/\W/\s to their XSD value spaces) collapses Regex 120→69;
149
149
  same-step pattern facets now behave as alternatives and
150
150
  whitespace-only text reaches the facets intact (PR #200), Regex
@@ -612,6 +612,78 @@ tuple keys, node-set field violations and absent-field rows handled, cross-eleme
612
612
  the RAW validity boolean — expected-INVALID rows reading false
613
613
  are PASSES; the first assembly inverted them and manufactured 9
614
614
  phantom reZ regressions before the correction.
615
+ - QUADRATIC-DISPATCH ROUND (2026-11-12, release pending): the
616
+ per-rule profile on reZ003v (a trivial `[\w]` pattern over a
617
+ 232KB instance of ~65k `<value>` siblings) showed the RULES at
618
+ ~3.3s and the DISPATCH at ~100-370s — `same_name_sibling_index`
619
+ scanned the parent's raw children per element (O(N) per child,
620
+ O(N²) per parent) TIMES five category walks, and
621
+ `positional_children` re-walked the parent's particle tree per
622
+ element. THREE cuts, all semantics-preserving: (a) the sibling
623
+ ordinal is now a per-parent identity-keyed map built in ONE
624
+ O(N) pass (running same-name count — identical to the old
625
+ take(position).count reading); (b) the dispatch skips the scan
626
+ when the content model holds at most ONE same-named candidate
627
+ (positional binding only matters for DUPLICATE declarations,
628
+ isDefault079); (c) positional_children memoizes per
629
+ (declaration, name) on the resolver (schema immutable once
630
+ indexed). reZ003v validate 373.6s → 2.94s (127×); rspec 1663
631
+ green and 40s faster; rubocop clean. LEDGER: 4649/4696
632
+ (98.91%) — the FIRST FULL-DENOMINATOR MEASURE of the campaign:
633
+ ALL 94 batches completed in one rake run, the 5 reZ budget
634
+ exclusions RETIRED (reZ003v/005v/005i/006v/006i measured and
635
+ PASSING in-run), and the batch-03000 carry-over convention
636
+ RETIRED (reI003's tax was the same dispatch overhead, not regex
637
+ backtracking — 50/50 fresh). ZERO regressions vs the 1.2.112
638
+ ledger (47 still-failing in both — the standing remainder:
639
+ contested 24, the upstream filings' legs, the parked redesign
640
+ tails).
641
+ - REMAINDER-CLASSIFICATION ROUND (2026-11-12, release pending):
642
+ the live ledger re-derived the queue (the memory's schA1/D10
643
+ gaps were STALE — all three pass since the chameleon/type-
644
+ binding rounds); the 47-case remainder splits into 13
645
+ filed-upstream, ~25 contested/disputed, and NINE ours. ALL
646
+ NINE FIXED, zero regressions — four mechanism families:
647
+ (a) DERIVATION COMPLETENESS (stZ054/055/056/073b/ba): §4.1.2
648
+ anySimpleType is the ur-simple root — EVERY simple type derives
649
+ from it, restriction-of-builtin chains and pure lists/unions
650
+ alike, and a simpleContent derivation over a builtin reaches it
651
+ transitively; a BUILTIN override owes the §3.3.4 check against
652
+ NAMED declared types via the BUILTIN_ANCESTORS table
653
+ (ValueSpace) — identity or declared-in-ancestry, xs:anyType
654
+ derives from nothing but itself (stZ056), restriction-of-union
655
+ does NOT admit members (stZ073b), direct unions do (the
656
+ elemT071 rule, builtin members included). (b) NFA LOOP-BRANCH
657
+ BYPASS (particlesA013 + the matcher probe): with_occurrence's
658
+ unbounded branch wired ε-edges from the FIRST body copy's exit
659
+ to final — with min ≥ 2 those bypass the mandatory unrolled
660
+ copies and accept after one consumption; the edges are
661
+ redundant at min ≤ 1 (cursor == exit_state) and removed.
662
+ (c) CHOICE-MIN EMPTY BRANCH (particlesIe004): a branch whose
663
+ minimum is zero satisfies the choice with the EMPTY sequence —
664
+ the choice's own minimum cannot fail on a zero count.
665
+ (d) BLOCK IN WHOLE OR IN PART + EXTENSION TEXT (particlesIg003,
666
+ addB114): §3.3.4 blocks the derivation CHAIN — any_type_chain_
667
+ kinds walks every hop to the ur-type (a bare complexType
668
+ restricts anyType per §3.4.3, so ext→base→anyType carries a
669
+ restriction hop that block="restriction" catches), and a
670
+ simpleContent EXTENSION's text validates against the
671
+ extension's base type (addB114: '62' violates base A's facets
672
+ through E-A; the branch previously had only a comment).
673
+ Also routed products AT the OccurrenceProductGuard cap to the
674
+ Ruby NFA (`< CAP` — the engine answers lax on wildcard
675
+ minimums there) and admitted the parsed Xml::Schema::Xsd::Any
676
+ class to PARTICLE_TYPES + the matcher wildcard dispatch.
677
+ SUITE 4658/4696 (99.19%) — first measure above 99; rspec 1663
678
+ green; rubocop clean. REMAINDER 38: 13 filed-upstream
679
+ (leptris-rs#72/#73, #45 legs, attgC035-038), ~25
680
+ contested/disputed (reJ 14, reS/reT/reU/reDH vintages,
681
+ attZ014a/b 1.1 drift, gMonth_pattern/double018, particlesZ001).
682
+ TOOLING NOTE: rubocop -A broke the tri-state
683
+ builtin_derivation_cover — the nil ("not a builtin") became
684
+ false, which would have flagged every schema-type override
685
+ undervalued; caught by re-reading the -A diff (the standing
686
+ predicate-nil lesson, now with the rename fix applied).
615
687
  - TRAIN ADOPTION REGRESSION — CHARACTER REFERENCES (2026-11-11):
616
688
  the 2026-11-10 adoption verdict ("conformance-IDENTICAL") was
617
689
  WRONG for one class: moxml 0.6.4 stopped decoding numeric
@@ -195,7 +195,7 @@ module Lutaml
195
195
  with_occurrence(particle) { compile_all(particle) }
196
196
  when Lutaml::Xml::Schema::Xsd::Group
197
197
  compile_group(particle)
198
- when Lutaml::Xsd::Any
198
+ when Lutaml::Xsd::Any, Lutaml::Xml::Schema::Xsd::Any
199
199
  with_occurrence(particle) { leaf_fragment(:wildcard, particle) }
200
200
  else
201
201
  with_occurrence(particle) { leaf_fragment(:element, particle) }
@@ -330,10 +330,13 @@ module Lutaml
330
330
  @epsilons[entry] << final if min.zero?
331
331
  if max == UNBOUNDED || min > MAX_UNROLL ||
332
332
  (max != UNBOUNDED && max - min > MAX_UNROLL)
333
+ # Loop-or-finish from the LAST mandatory copy's exit only.
334
+ # The first copy's exit must NOT shortcut to final — with
335
+ # min >= 2 the ε-edge would bypass the mandatory copies and
336
+ # accept after one consumption (particlesA013: any min=2
337
+ # matched by a single child).
333
338
  @epsilons[cursor] << last_entry
334
339
  @epsilons[cursor] << final
335
- @epsilons[exit_state] << last_entry
336
- @epsilons[exit_state] << final
337
340
  else
338
341
  @epsilons[cursor] << final
339
342
  optional = max - min - (min.zero? && max.is_a?(Integer) ? 1 : 0)
@@ -62,7 +62,11 @@ module Lutaml
62
62
  end
63
63
 
64
64
  def particles_safe?(particles)
65
- particles.all? { |particle| product(particle, 1, 0) <= CAP }
65
+ # Products AT the cap route to the Ruby NFA too — the engine
66
+ # compiles them into exactly the automata this guard exists
67
+ # to avoid (particlesA013: any min=2 max=1000000; the engine
68
+ # answers lax on the wildcard minimum there).
69
+ particles.all? { |particle| product(particle, 1, 0) < CAP }
66
70
  end
67
71
 
68
72
  # Walks a type's content model. Nested elements referencing
@@ -80,7 +84,7 @@ module Lutaml
80
84
  expanded = @group_expander&.call(particle)
81
85
  expanded && particles_safe?(expanded)
82
86
  else
83
- product(particle, 1, 0) <= CAP
87
+ product(particle, 1, 0) < CAP
84
88
  end
85
89
  end
86
90
  end
@@ -80,8 +80,17 @@ module Lutaml
80
80
  resolver.resolve(parent.name, parent.namespace_uri)
81
81
  candidates = resolver.positional_children(parent_declaration,
82
82
  xml_element.name)
83
- positional = candidates[same_name_sibling_index(xml_element,
84
- parent)]
83
+ # The scan only matters when the parent's content model
84
+ # holds DUPLICATE same-named declarations (isDefault079);
85
+ # a sole candidate binds regardless of position — skipping
86
+ # the sibling scan keeps wide documents linear.
87
+ positional = if candidates.size > 1
88
+ candidates[same_name_sibling_index(
89
+ xml_element, parent
90
+ )]
91
+ else
92
+ candidates.first
93
+ end
85
94
  # The binding must agree with the child's OWN namespace:
86
95
  # an unqualified ref's global namesake never serves a
87
96
  # qualified local namesake (particlesQ016).
@@ -245,21 +254,31 @@ module Lutaml
245
254
  override && override.value.to_s.split(":").last
246
255
  end
247
256
 
248
- # The child's position among its same-named siblings — the index
257
+ # The child's ordinal among its same-named siblings — the index
249
258
  # the positional binding (duplicate same-named declarations,
250
- # isDefault079) binds to. Computed on the RAW moxml nodes: the
259
+ # isDefault079) binds to. Keyed on the RAW moxml nodes: the
251
260
  # wrapper objects are not identity-stable across accessor calls
252
- # (the IdUniquenessRule lesson).
261
+ # (the IdUniquenessRule lesson). The per-parent ordinal map is
262
+ # memoized — a document of N same-named siblings costs one
263
+ # O(N) pass per parent instead of an O(N) scan per child
264
+ # (reZ003v: 65k siblings made every dispatch quadratic, times
265
+ # five category walks).
253
266
  #
254
267
  # @return [Integer]
255
268
  def same_name_sibling_index(element, parent)
256
- raw_children = Array(parent.moxml_element.elements)
257
- position = raw_children.index do |node|
258
- node.equal?(element.moxml_element)
269
+ parent_raw = parent.moxml_element
270
+ cache = (@sibling_ordinals ||= {}.compare_by_identity)
271
+ ordinals = cache[parent_raw]
272
+ if ordinals.nil?
273
+ ordinals = {}.compare_by_identity
274
+ counts = Hash.new(0)
275
+ parent_raw.elements.each do |node|
276
+ ordinals[node] = counts[node.name]
277
+ counts[node.name] += 1
278
+ end
279
+ cache[parent_raw] = ordinals
259
280
  end
260
- return 0 if position.nil?
261
-
262
- raw_children.take(position).count { |node| node.name == element.name }
281
+ ordinals.fetch(element.moxml_element, 0)
263
282
  end
264
283
 
265
284
  # Whether the parent's content model holds a processContents
@@ -37,6 +37,7 @@ module Lutaml
37
37
  PARTICLE_TYPES = [
38
38
  Lutaml::Xsd::Element,
39
39
  Lutaml::Xsd::Any,
40
+ Lutaml::Xml::Schema::Xsd::Any,
40
41
  Lutaml::Xsd::Sequence,
41
42
  Lutaml::Xsd::Choice,
42
43
  Lutaml::Xsd::All,
@@ -415,7 +415,15 @@ module Lutaml
415
415
  repository)
416
416
  occurrence_count = choice_occurrence_count(present_count, choice, repository)
417
417
 
418
- if occurrence_count < min_occurs
418
+ # A branch whose occurrence minimum is zero satisfies the
419
+ # choice with the EMPTY sequence (XSD particle semantics),
420
+ # so the choice's own minimum cannot fail on a zero count
421
+ # (particlesIe004: choice(e1 1, e2 0) against an empty doc).
422
+ empty_branch = choice_alternatives(choice).any? do |alt|
423
+ parse_occurs(alt.min_occurs, 1).zero?
424
+ end
425
+
426
+ if occurrence_count < min_occurs && !empty_branch
419
427
  report_error(
420
428
  collector,
421
429
  code: "choice_min_occurs_violation",
@@ -26,6 +26,14 @@ module Lutaml
26
26
  include EngineBacked
27
27
  include ValueSpace
28
28
 
29
+ # The ur-simple root: every simple type derives from it
30
+ # (XSD 1.0 §4.1.2).
31
+ ANY_SIMPLE_TYPE_LOCAL = "anySimpleType"
32
+
33
+ # The ur-type: root of the type hierarchy, derives from
34
+ # nothing but itself.
35
+ ANY_TYPE_LOCAL = "anyType"
36
+
29
37
  # Rule category
30
38
  #
31
39
  # @return [Symbol] :type
@@ -62,14 +70,14 @@ module Lutaml
62
70
  repository)
63
71
  unless type_def
64
72
  # xsi:type may name a built-in — resolve_named_type only
65
- # sees schema documents (addB028).
66
- if builtin_override_reference(xml_element)
67
- validate_builtin_type(xml_element,
68
- builtin_override_reference(xml_element),
69
- collector,
70
- declared: schema_element)
71
- return
72
- end
73
+ # sees schema documents (addB028). A builtin override
74
+ # still owes the §3.3.4 derivation against the declared
75
+ # type: anyType derives from nothing else, so overriding
76
+ # an anySimpleType declaration with xs:anyType is
77
+ # invalid (stZ056).
78
+ return if builtin_override_reference(xml_element) &&
79
+ builtin_override_handled?(xml_element, schema_element,
80
+ collector, repository)
73
81
 
74
82
  report_error(
75
83
  collector,
@@ -136,7 +144,9 @@ module Lutaml
136
144
  # declaration's own fixed content (isDefault077).
137
145
  validate_fixed_content(xml_element, schema_element,
138
146
  xml_element.text_content, collector)
139
- validate_complex_type_content(xml_element, type_def, collector)
147
+ validate_complex_type_content(xml_element, type_def, collector,
148
+ repository: repository,
149
+ declared: schema_element)
140
150
  else
141
151
  # Handle built-in types
142
152
  validate_builtin_type(xml_element, type_def, collector,
@@ -172,7 +182,12 @@ module Lutaml
172
182
  # from one (elemT071); the override's block-kind comes from
173
183
  # the member chain (elemT074). Complex types derive through
174
184
  # both content kinds — a simpleContent extension of a simple
175
- # base is a derivation too (elemT058).
185
+ # base is a derivation too (elemT058). anySimpleType is the
186
+ # ur-simple root (§4.1.2): EVERY simple type derives from it
187
+ # — restriction-of-builtin chains and pure lists/unions alike
188
+ # (stZ054) — and a simpleContent derivation over a builtin
189
+ # base reaches it transitively (stZ055's extension of
190
+ # xs:string).
176
191
  #
177
192
  # @return [Boolean]
178
193
  def validly_derived?(type, base_local, repository, depth = 0)
@@ -190,14 +205,19 @@ module Lutaml
190
205
  part = derivation_part(type)
191
206
  return false unless part
192
207
 
193
- next_local = part.base.to_s.split(":").last
208
+ raw_base = part.base.to_s
209
+ next_local = raw_base.split(":").last
194
210
  return true if next_local == base_local
211
+ return true if base_local == ANY_SIMPLE_TYPE_LOCAL &&
212
+ builtin_base?(raw_base)
195
213
 
196
214
  validly_derived?(
197
215
  repository&.resolve_named_type(next_local),
198
216
  base_local, repository, depth + 1
199
217
  )
200
218
  when Lutaml::Xml::Schema::Xsd::SimpleType
219
+ return true if base_local == ANY_SIMPLE_TYPE_LOCAL
220
+
201
221
  restriction = type.restriction
202
222
  return false unless restriction
203
223
 
@@ -231,6 +251,18 @@ module Lutaml
231
251
  end
232
252
  end
233
253
 
254
+ # Whether a derivation base attribute names a BUILTIN type —
255
+ # the xs:/xsd:-prefixed spellings only. An unresolvable
256
+ # UNPREFIXED base is a lenient-fallback miss, never a builtin
257
+ # (the string-fallback lesson).
258
+ #
259
+ # @param raw_base [String]
260
+ # @return [Boolean]
261
+ def builtin_base?(raw_base)
262
+ prefix = raw_base.to_s.split(":", 2).first
263
+ ValueSpace::BUILTIN_PREFIXES.include?(prefix)
264
+ end
265
+
234
266
  # XSD 1.0 §3.3.4 / §3.4.6: block on the element declaration
235
267
  # AND on the declared complexType forbids replacing the type
236
268
  # with one derived by the blocked kind. A type-less
@@ -241,15 +273,20 @@ module Lutaml
241
273
  # @return [Boolean]
242
274
  def override_blocked?(schema_element, override_type, repository)
243
275
  declared_local = schema_element.type.to_s.split(":").last
244
- kind =
276
+ kinds =
245
277
  if declared_local.to_s.empty?
246
- any_type_derivation_kind(override_type)
278
+ any_type_chain_kinds(override_type, repository)
247
279
  else
248
- derivation_kind_from(override_type, declared_local) ||
249
- member_derivation_kind(override_type, declared_local,
250
- repository)
280
+ direct = derivation_kind_from(override_type, declared_local)
281
+ direct ||= member_derivation_kind(override_type,
282
+ declared_local,
283
+ repository)
284
+ direct ||= derivation_chain_kinds(override_type,
285
+ declared_local,
286
+ repository).first
287
+ Array(direct)
251
288
  end
252
- return false unless kind
289
+ return false if kinds.empty?
253
290
 
254
291
  tokens = effective_block_tokens(schema_element, repository)
255
292
  declared = repository&.resolve_named_type(declared_local)
@@ -261,7 +298,68 @@ module Lutaml
261
298
  tokens.intersect?(%w[extension restriction])
262
299
 
263
300
  blocked = tokens.include?("#all") ? %w[extension restriction] : tokens
264
- blocked.include?(kind)
301
+ kinds.any? { |kind| blocked.include?(kind) }
302
+ end
303
+
304
+ # §3.3.4 clause 2.2.2: the block applies to the derivation
305
+ # IN WHOLE OR IN PART — the kinds of EVERY hop from the
306
+ # override down to the declared type (ctI's indirect shapes).
307
+ #
308
+ # @return [Array<String>]
309
+ def derivation_chain_kinds(type, declared_local, repository,
310
+ depth = 0)
311
+ return [] if type.nil? || declared_local.to_s.empty? || depth > 10
312
+
313
+ case type
314
+ when Lutaml::Xml::Schema::Xsd::ComplexType
315
+ part = derivation_part(type)
316
+ return [] unless part
317
+
318
+ kind = part.class.name.include?("Extension") ? "extension" : "restriction"
319
+ next_local = part.base.to_s.split(":").last
320
+ return [kind] if next_local == declared_local
321
+
322
+ [kind, *derivation_chain_kinds(
323
+ repository&.resolve_named_type(next_local), declared_local,
324
+ repository, depth + 1
325
+ )]
326
+ when Lutaml::Xml::Schema::Xsd::SimpleType
327
+ restriction = type.restriction
328
+ return [] unless restriction
329
+
330
+ next_local = restriction.base.to_s.split(":").last
331
+ return ["restriction"] if next_local == declared_local
332
+
333
+ derivation_chain_kinds(
334
+ repository&.resolve_named_type(next_local), declared_local,
335
+ repository, depth + 1
336
+ )
337
+ end
338
+ end
339
+
340
+ # Kinds along the override's whole chain down to the ur-type
341
+ # — a type-less declaration's block governs every hop
342
+ # (particlesIg003: ext extends base, base restricts anyType).
343
+ #
344
+ # @return [Array<String>]
345
+ def any_type_chain_kinds(type, repository, depth = 0)
346
+ return [] if type.nil? || depth > 10
347
+
348
+ if type.is_a?(Lutaml::Xml::Schema::Xsd::SimpleType)
349
+ return ["restriction"]
350
+ end
351
+
352
+ part = derivation_part(type)
353
+ # A bare complexType (no derivation marker) restricts the
354
+ # ur-type per §3.4.3.
355
+ return ["restriction"] unless part
356
+
357
+ kind = part.class.name.include?("Extension") ? "extension" : "restriction"
358
+ next_local = part.base.to_s.split(":").last
359
+ [kind, *any_type_chain_kinds(
360
+ repository&.resolve_named_type(next_local), repository,
361
+ depth + 1
362
+ )]
265
363
  end
266
364
 
267
365
  # Derivation kind of an override against a type-less
@@ -418,6 +516,95 @@ module Lutaml
418
516
  "#{prefix}:#{local}"
419
517
  end
420
518
 
519
+ # XSD 1.0 §3.3.4 clause 2.2: a builtin override owes valid
520
+ # derivation from the declared type. anyType derives from
521
+ # nothing but itself; every other builtin derives from
522
+ # anySimpleType (§4.1.2 ancestry table); a named declared
523
+ # type must BE the builtin or one of its ancestors
524
+ # (stZ073b: xs:integer is no derivation of the
525
+ # restriction-of-union st.unionType); a declared UNION admits
526
+ # overrides naming a member or a builtin derived from one
527
+ # (the elemT071 rule, builtin members included).
528
+ #
529
+ # @return [Boolean]
530
+ def builtin_override_undervalued?(xml_element, schema_element,
531
+ repository)
532
+ override = xsi_type_name(xml_element).to_s
533
+ declared = declared_local_name(schema_element)
534
+ return false if declared.empty? || declared == override
535
+ return true if override == ANY_TYPE_LOCAL
536
+ return false if declared == ANY_SIMPLE_TYPE_LOCAL
537
+
538
+ covered = ValueSpace.builtin_derivation_cover(declared,
539
+ override)
540
+ return !covered unless covered.nil?
541
+
542
+ declared_type = repository&.resolve_named_type(declared)
543
+ return false unless declared_type.is_a?(
544
+ Lutaml::Xml::Schema::Xsd::SimpleType,
545
+ )
546
+
547
+ if declared_type.union
548
+ return !union_member_covers_builtin?(declared_type, override,
549
+ repository)
550
+ end
551
+
552
+ !validly_derived?(declared_type, override, repository)
553
+ end
554
+
555
+ # Whether the override builtin is admitted by a declared
556
+ # union: it names a member, or a member's chain reaches it.
557
+ #
558
+ # @return [Boolean]
559
+ def union_member_covers_builtin?(union_type, override, repository)
560
+ members = Array(union_type.union.member_types)
561
+ members.any? do |member|
562
+ local = member.to_s.split(":").last
563
+ next true if local == override
564
+
565
+ member_type = repository&.resolve_named_type(local)
566
+ next false unless member_type
567
+
568
+ validly_derived?(member_type, override, repository)
569
+ end
570
+ end
571
+
572
+ # Handles an element whose xsi:type names a built-in: the
573
+ # §3.3.4 derivation gate, then builtin content validation.
574
+ #
575
+ # @return [Boolean] true when the element was handled
576
+ def builtin_override_handled?(xml_element, schema_element,
577
+ collector, repository)
578
+ if builtin_override_undervalued?(xml_element, schema_element,
579
+ repository)
580
+ report_error(
581
+ collector,
582
+ code: "xsi_type_not_derived",
583
+ message: "xsi:type '#{xsi_type_name(xml_element)}' is " \
584
+ "not validly derived from the declared type " \
585
+ "'#{declared_local_name(schema_element)}'",
586
+ location: xml_element.xpath,
587
+ context: { xsi_type: xsi_type_name(xml_element) },
588
+ )
589
+ return true
590
+ end
591
+
592
+ validate_builtin_type(xml_element,
593
+ builtin_override_reference(xml_element),
594
+ collector,
595
+ declared: schema_element)
596
+ true
597
+ end
598
+
599
+ # The declared type's local name ('' when the declaration is
600
+ # type-less — the implicit anyType).
601
+ #
602
+ # @param schema_element [Object]
603
+ # @return [String]
604
+ def declared_local_name(schema_element)
605
+ schema_element.type.to_s.split(":").last.to_s
606
+ end
607
+
421
608
  # Resolve the type definition for an element
422
609
  #
423
610
  # @param schema_element [Lutaml::Xsd::Element, Lutaml::Xsd::ComplexType] Schema element
@@ -566,7 +753,8 @@ module Lutaml
566
753
  # @param collector [ResultCollector] Result collector
567
754
  # @return [void]
568
755
  def validate_complex_type_content(xml_element, complex_type,
569
- collector, repository: nil)
756
+ collector, repository: nil,
757
+ declared: nil)
570
758
  # Complex type validation is handled by:
571
759
  # - AttributeValidationRule for attributes
572
760
  # - ContentModelValidationRule for content model
@@ -575,7 +763,8 @@ module Lutaml
575
763
  # Check for simple content
576
764
  if complex_type.simple_content
577
765
  validate_simple_content(xml_element, complex_type.simple_content,
578
- collector, repository: repository)
766
+ collector, repository: repository,
767
+ declared: declared)
579
768
  end
580
769
  end
581
770
 
@@ -1044,12 +1233,26 @@ module Lutaml
1044
1233
  # @param collector [ResultCollector] Result collector
1045
1234
  # @return [void]
1046
1235
  def validate_simple_content(xml_element, simple_content, collector,
1047
- repository: nil)
1236
+ repository: nil, declared: nil)
1048
1237
  # Simple content means element has text content and attributes
1049
1238
  # Validate the text content as a simple type
1050
1239
  if simple_content.extension
1051
- # Extension adds attributes to a simple type
1052
- # Text content validation handled by base type
1240
+ # The extension's TEXT still validates against the
1241
+ # extension's base type — the extension itself only adds
1242
+ # attributes (addB114: '62' violates base A's enumeration
1243
+ # and range through E-A's simpleContent extension).
1244
+ raw_base = simple_content.extension.base.to_s
1245
+ base_local = raw_base.split(":").last
1246
+ base_type = repository&.resolve_named_type(base_local)
1247
+ if base_type.is_a?(Lutaml::Xml::Schema::Xsd::SimpleType)
1248
+ validate_simple_type_content(xml_element, base_type,
1249
+ collector,
1250
+ repository: repository,
1251
+ declared: declared)
1252
+ elsif builtin_base?(raw_base)
1253
+ validate_builtin_type(xml_element, raw_base, collector,
1254
+ declared: declared)
1255
+ end
1053
1256
  elsif simple_content.restriction
1054
1257
  # Restriction constrains a simple type
1055
1258
  value = xml_element.text_content
@@ -607,17 +607,28 @@ module Lutaml
607
607
  # @param parent_declaration [Lutaml::Xml::Schema::Xsd::Element, nil]
608
608
  # @param local_name [String]
609
609
  # @return [Array<Lutaml::Xml::Schema::Xsd::Element>]
610
+ #
611
+ # Memoized per (declaration, local name): the dispatch calls
612
+ # this for every element, and the particle-tree walk cost is
613
+ # constant across elements of the same parent type (the schema
614
+ # is immutable once indexed).
610
615
  def positional_children(parent_declaration, local_name)
616
+ key = [parent_declaration.object_id, local_name]
617
+ cache = (@positional_children ||= {})
618
+ cached = cache[key]
619
+ return cached if cached
620
+
611
621
  type = parent_declaration&.complex_type ||
612
622
  resolve_type(parent_declaration)
613
- return [] unless type.is_a?(Lutaml::Xml::Schema::Xsd::ComplexType)
614
-
615
- particles = []
616
- collect_named_particles(particle_for_type(type), local_name,
617
- particles, {}.compare_by_identity, 0)
618
- return [] unless particles.all? { |p| @namespaces.key?(p) }
619
-
620
- particles
623
+ computed = []
624
+ if type.is_a?(Lutaml::Xml::Schema::Xsd::ComplexType)
625
+ particles = []
626
+ collect_named_particles(particle_for_type(type), local_name,
627
+ particles, {}.compare_by_identity, 0)
628
+ computed = particles if particles.all? { |p| @namespaces.key?(p) }
629
+ end
630
+ cache[key] = computed
631
+ computed
621
632
  end
622
633
 
623
634
  private
@@ -66,6 +66,79 @@ module Lutaml
66
66
 
67
67
  BUILTIN_PREFIXES = %w[xs xsd].freeze
68
68
 
69
+ # The built-in type hierarchy (XSD 1.0 §4.1.2, Figure 4-1 +
70
+ # §3.14.6): each local name maps to its ANCESTOR builtins.
71
+ # A builtin xsi:type override is validly derived from a
72
+ # declared type only when the declared name is the override
73
+ # itself or one of its ancestors (§3.3.4 clause 2.2).
74
+ BUILTIN_ANCESTORS = {
75
+ "anyType" => [].freeze,
76
+ "anySimpleType" => %w[anyType].freeze,
77
+ "string" => %w[anySimpleType anyType].freeze,
78
+ "normalizedString" => %w[string anySimpleType anyType].freeze,
79
+ "token" => %w[normalizedString string anySimpleType anyType].freeze,
80
+ "language" => %w[token normalizedString string anySimpleType anyType].freeze,
81
+ "Name" => %w[token normalizedString string anySimpleType anyType].freeze,
82
+ "NCName" => %w[Name token normalizedString string anySimpleType anyType].freeze,
83
+ "ID" => %w[NCName Name token normalizedString string anySimpleType anyType].freeze,
84
+ "IDREF" => %w[NCName Name token normalizedString string anySimpleType anyType].freeze,
85
+ "NMTOKEN" => %w[token normalizedString string anySimpleType anyType].freeze,
86
+ "IDREFS" => %w[IDREF NCName Name token normalizedString string anySimpleType anyType].freeze,
87
+ "ENTITY" => %w[NCName Name token normalizedString string anySimpleType anyType].freeze,
88
+ "ENTITIES" => %w[ENTITY NCName Name token normalizedString string anySimpleType anyType].freeze,
89
+ "NMTOKENS" => %w[NMTOKEN token normalizedString string anySimpleType anyType].freeze,
90
+ "decimal" => %w[anySimpleType anyType].freeze,
91
+ "integer" => %w[decimal anySimpleType anyType].freeze,
92
+ "long" => %w[integer decimal anySimpleType anyType].freeze,
93
+ "int" => %w[long integer decimal anySimpleType anyType].freeze,
94
+ "short" => %w[int long integer decimal anySimpleType anyType].freeze,
95
+ "byte" => %w[short int long integer decimal anySimpleType anyType].freeze,
96
+ "nonNegativeInteger" => %w[integer decimal anySimpleType anyType].freeze,
97
+ "positiveInteger" => %w[nonNegativeInteger integer decimal anySimpleType anyType].freeze,
98
+ "unsignedLong" => %w[nonNegativeInteger integer decimal anySimpleType anyType].freeze,
99
+ "unsignedInt" => %w[unsignedLong nonNegativeInteger integer decimal anySimpleType anyType].freeze,
100
+ "unsignedShort" => %w[unsignedInt unsignedLong nonNegativeInteger integer decimal anySimpleType anyType].freeze,
101
+ "unsignedByte" => %w[unsignedShort unsignedInt unsignedLong nonNegativeInteger integer decimal anySimpleType anyType].freeze,
102
+ "nonPositiveInteger" => %w[integer decimal anySimpleType anyType].freeze,
103
+ "negativeInteger" => %w[nonPositiveInteger integer decimal anySimpleType anyType].freeze,
104
+ "float" => %w[anySimpleType anyType].freeze,
105
+ "double" => %w[anySimpleType anyType].freeze,
106
+ "duration" => %w[anySimpleType anyType].freeze,
107
+ "dateTime" => %w[anySimpleType anyType].freeze,
108
+ "time" => %w[anySimpleType anyType].freeze,
109
+ "date" => %w[anySimpleType anyType].freeze,
110
+ "gYear" => %w[anySimpleType anyType].freeze,
111
+ "gYearMonth" => %w[anySimpleType anyType].freeze,
112
+ "gMonth" => %w[anySimpleType anyType].freeze,
113
+ "gMonthDay" => %w[anySimpleType anyType].freeze,
114
+ "gDay" => %w[anySimpleType anyType].freeze,
115
+ "boolean" => %w[anySimpleType anyType].freeze,
116
+ "base64Binary" => %w[anySimpleType anyType].freeze,
117
+ "hexBinary" => %w[anySimpleType anyType].freeze,
118
+ "anyURI" => %w[anySimpleType anyType].freeze,
119
+ "QName" => %w[anySimpleType anyType].freeze,
120
+ "NOTATION" => %w[QName anySimpleType anyType].freeze,
121
+ }.freeze
122
+
123
+ # Whether a builtin override local is validly derived from the
124
+ # declared local: identity or the declared name sits in the
125
+ # override's builtin ancestry. TRI-STATE: nil when the override
126
+ # is not a builtin local at all (the caller falls through to
127
+ # schema-type resolution). No `?` suffix — rubocop -A rewrites
128
+ # nil-returning predicate guards to false and breaks the
129
+ # contract.
130
+ #
131
+ # @param declared [String] declared type local name
132
+ # @param override [String] override builtin local name
133
+ # @return [Boolean, nil]
134
+ def self.builtin_derivation_cover(declared, override)
135
+ ancestors = BUILTIN_ANCESTORS[override.to_s]
136
+ return nil unless ancestors
137
+
138
+ declared.to_s == override.to_s ||
139
+ ancestors.include?(declared.to_s)
140
+ end
141
+
69
142
  # xsd:length counts list ITEMS for list types, not characters.
70
143
  LIST_BASES = %w[IDREFS ENTITIES NMTOKENS].freeze
71
144
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Xsd
5
- VERSION = "1.2.112"
5
+ VERSION = "1.2.114"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lutaml-xsd
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.112
4
+ version: 1.2.114
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.