lutaml-xsd 1.2.113 → 1.2.115

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: 4ef5ebe654a09dce031fef237b193d2f537550b3f6e05e234214fe2c9bba8a21
4
- data.tar.gz: bde3cb46ee3b74ea6a2812d98b69c73e8ea5ad5f7a89a838350a290a76917eec
3
+ metadata.gz: 8361d002b4925f8e9617b1befc010cdf69eda020d9819ad6cb563d5716fd0cb3
4
+ data.tar.gz: d7ee489b009dbfe0f537b6ec11ea9e68435161619db337801a8b2fe44881b267
5
5
  SHA512:
6
- metadata.gz: df70a1340ca701bebd079dcd7b514f1657c5aa2a9e4117127a3003fdb0a63487aeb44e981da443b40801194e556be881f181d729cced3d0ed8fc07b08191e4f1
7
- data.tar.gz: 90623814fed971c9c310f00ea3f1de694b624a15d634a0452e263786bdb3b934891023431af787bfe0cfdc2146bef9f9a3ce29f201d86e85401d3304648c68a5
6
+ metadata.gz: a67d033744fd14e969811aed1d8b7f7e679a50b2c535a1ea649e670d5e4baf6f5fef3c5b11349cfef519dbfb0cdacdc467270ccf6c3c231d3f6f234b2c7f84b2
7
+ data.tar.gz: 02fec25f4e6c24ecf8c591e8fff9dfb245acaeacb41a09c36223ae6a1da1cdc8e35a22c215b2a11c137982b1d49c284c568e926faf49d856bcc52f68a13be1a3
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 | 4649/4696 (98.91%) — FIRST FULL-DENOMINATOR MEASURE, all 94 batches in one rake run |
123
+ | pipeline | 4664/4696 (99.32%) |
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) → 4649 (98.91%, quadratic-dispatch round — first FULL-denominator measure, the 5 reZ budget exclusions retired), +688 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) → 4664 (99.32%, ride-419 round), +703 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
@@ -638,6 +638,84 @@ tuple keys, node-set field violations and absent-field rows handled, cross-eleme
638
638
  ledger (47 still-failing in both — the standing remainder:
639
639
  contested 24, the upstream filings' legs, the parked redesign
640
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).
687
+ - RIDE-419 ROUND (2026-11-13, release pending): leptris
688
+ 1.9.419.0 closed BOTH overnight filings — leptris-rs#72
689
+ (wildZ013b/c: the namespace-less-import wildcard admission
690
+ restored) and #73 (attZ014a/b: the multi-xs:ID 1.0 reading
691
+ restored). The ride also surfaced that OUR fixed-value check
692
+ was lexical-only where the suite demands VALUE-space equality
693
+ through a union's first-accepting member (§4.1.7): stE050
694
+ ('true' == fixed '1' as boolean-true) and stE055 (float forms)
695
+ now pass via ValueSpace.union_member_value_key + the
696
+ fixed_value_space_equal? comparison in TypeValidationRule
697
+ (union members split per the established member_types.to_s.
698
+ split pattern — Array() on the attribute yields ONE
699
+ space-separated string, which silently never matches; three
700
+ call sites corrected, including a latent one from the previous
701
+ round). stE065/066/096 remain ENGINE-bound — filed
702
+ leptris-rs#74 (the engine's own fixed-value comparison is
703
+ lexical-only for unions); attgC035-038 remain FACADE-bound —
704
+ filed moxml#545 (the closed-#522 thread's uncovered legs: the
705
+ strict re-check false-reports attributeGroup-redefine §4.2.2
706
+ hook members). SUITE 4664/4696 (99.32%), zero regressions;
707
+ rspec 1663 green; rubocop clean. INCIDENT (recovered
708
+ in-round): threading repository: into validate_fixed_content
709
+ via a bulk gsub landed a call inside validate_builtin_type,
710
+ whose scope has no repository local — NameError on every
711
+ builtin-typed element, a 686-case regression wave that
712
+ briefly looked like a leptris-419 breakage; the pin-back
713
+ battery exonerated the train (the wave reproduced on 418)
714
+ and the signature fix recovered the battery. LESSON: when
715
+ threading a parameter through call sites, verify the RECEIVER
716
+ method's signature carries it — a mechanical gsub crosses
717
+ scope boundaries silently, and the crash-as-invalid
718
+ convention turns the NameError into wholesale false verdicts.
641
719
  - TRAIN ADOPTION REGRESSION — CHARACTER REFERENCES (2026-11-11):
642
720
  the 2026-11-10 adoption verdict ("conformance-IDENTICAL") was
643
721
  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
@@ -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,
@@ -119,7 +127,8 @@ module Lutaml
119
127
  # A type-less declaration is xs:anyType; its fixed value
120
128
  # still constrains the lexical content (isDefault070).
121
129
  validate_fixed_content(xml_element, schema_element,
122
- xml_element.text_content, collector)
130
+ xml_element.text_content, collector,
131
+ repository: repository)
123
132
  return
124
133
  end
125
134
 
@@ -135,12 +144,16 @@ module Lutaml
135
144
  # complex walk checks the type's model, this checks the
136
145
  # declaration's own fixed content (isDefault077).
137
146
  validate_fixed_content(xml_element, schema_element,
138
- xml_element.text_content, collector)
139
- validate_complex_type_content(xml_element, type_def, collector)
147
+ xml_element.text_content, collector,
148
+ repository: repository)
149
+ validate_complex_type_content(xml_element, type_def, collector,
150
+ repository: repository,
151
+ declared: schema_element)
140
152
  else
141
153
  # Handle built-in types
142
154
  validate_builtin_type(xml_element, type_def, collector,
143
- declared: schema_element)
155
+ declared: schema_element,
156
+ repository: repository)
144
157
  end
145
158
  end
146
159
 
@@ -172,7 +185,12 @@ module Lutaml
172
185
  # from one (elemT071); the override's block-kind comes from
173
186
  # the member chain (elemT074). Complex types derive through
174
187
  # both content kinds — a simpleContent extension of a simple
175
- # base is a derivation too (elemT058).
188
+ # base is a derivation too (elemT058). anySimpleType is the
189
+ # ur-simple root (§4.1.2): EVERY simple type derives from it
190
+ # — restriction-of-builtin chains and pure lists/unions alike
191
+ # (stZ054) — and a simpleContent derivation over a builtin
192
+ # base reaches it transitively (stZ055's extension of
193
+ # xs:string).
176
194
  #
177
195
  # @return [Boolean]
178
196
  def validly_derived?(type, base_local, repository, depth = 0)
@@ -190,14 +208,19 @@ module Lutaml
190
208
  part = derivation_part(type)
191
209
  return false unless part
192
210
 
193
- next_local = part.base.to_s.split(":").last
211
+ raw_base = part.base.to_s
212
+ next_local = raw_base.split(":").last
194
213
  return true if next_local == base_local
214
+ return true if base_local == ANY_SIMPLE_TYPE_LOCAL &&
215
+ builtin_base?(raw_base)
195
216
 
196
217
  validly_derived?(
197
218
  repository&.resolve_named_type(next_local),
198
219
  base_local, repository, depth + 1
199
220
  )
200
221
  when Lutaml::Xml::Schema::Xsd::SimpleType
222
+ return true if base_local == ANY_SIMPLE_TYPE_LOCAL
223
+
201
224
  restriction = type.restriction
202
225
  return false unless restriction
203
226
 
@@ -231,6 +254,18 @@ module Lutaml
231
254
  end
232
255
  end
233
256
 
257
+ # Whether a derivation base attribute names a BUILTIN type —
258
+ # the xs:/xsd:-prefixed spellings only. An unresolvable
259
+ # UNPREFIXED base is a lenient-fallback miss, never a builtin
260
+ # (the string-fallback lesson).
261
+ #
262
+ # @param raw_base [String]
263
+ # @return [Boolean]
264
+ def builtin_base?(raw_base)
265
+ prefix = raw_base.to_s.split(":", 2).first
266
+ ValueSpace::BUILTIN_PREFIXES.include?(prefix)
267
+ end
268
+
234
269
  # XSD 1.0 §3.3.4 / §3.4.6: block on the element declaration
235
270
  # AND on the declared complexType forbids replacing the type
236
271
  # with one derived by the blocked kind. A type-less
@@ -241,15 +276,20 @@ module Lutaml
241
276
  # @return [Boolean]
242
277
  def override_blocked?(schema_element, override_type, repository)
243
278
  declared_local = schema_element.type.to_s.split(":").last
244
- kind =
279
+ kinds =
245
280
  if declared_local.to_s.empty?
246
- any_type_derivation_kind(override_type)
281
+ any_type_chain_kinds(override_type, repository)
247
282
  else
248
- derivation_kind_from(override_type, declared_local) ||
249
- member_derivation_kind(override_type, declared_local,
250
- repository)
283
+ direct = derivation_kind_from(override_type, declared_local)
284
+ direct ||= member_derivation_kind(override_type,
285
+ declared_local,
286
+ repository)
287
+ direct ||= derivation_chain_kinds(override_type,
288
+ declared_local,
289
+ repository).first
290
+ Array(direct)
251
291
  end
252
- return false unless kind
292
+ return false if kinds.empty?
253
293
 
254
294
  tokens = effective_block_tokens(schema_element, repository)
255
295
  declared = repository&.resolve_named_type(declared_local)
@@ -261,7 +301,68 @@ module Lutaml
261
301
  tokens.intersect?(%w[extension restriction])
262
302
 
263
303
  blocked = tokens.include?("#all") ? %w[extension restriction] : tokens
264
- blocked.include?(kind)
304
+ kinds.any? { |kind| blocked.include?(kind) }
305
+ end
306
+
307
+ # §3.3.4 clause 2.2.2: the block applies to the derivation
308
+ # IN WHOLE OR IN PART — the kinds of EVERY hop from the
309
+ # override down to the declared type (ctI's indirect shapes).
310
+ #
311
+ # @return [Array<String>]
312
+ def derivation_chain_kinds(type, declared_local, repository,
313
+ depth = 0)
314
+ return [] if type.nil? || declared_local.to_s.empty? || depth > 10
315
+
316
+ case type
317
+ when Lutaml::Xml::Schema::Xsd::ComplexType
318
+ part = derivation_part(type)
319
+ return [] unless part
320
+
321
+ kind = part.class.name.include?("Extension") ? "extension" : "restriction"
322
+ next_local = part.base.to_s.split(":").last
323
+ return [kind] if next_local == declared_local
324
+
325
+ [kind, *derivation_chain_kinds(
326
+ repository&.resolve_named_type(next_local), declared_local,
327
+ repository, depth + 1
328
+ )]
329
+ when Lutaml::Xml::Schema::Xsd::SimpleType
330
+ restriction = type.restriction
331
+ return [] unless restriction
332
+
333
+ next_local = restriction.base.to_s.split(":").last
334
+ return ["restriction"] if next_local == declared_local
335
+
336
+ derivation_chain_kinds(
337
+ repository&.resolve_named_type(next_local), declared_local,
338
+ repository, depth + 1
339
+ )
340
+ end
341
+ end
342
+
343
+ # Kinds along the override's whole chain down to the ur-type
344
+ # — a type-less declaration's block governs every hop
345
+ # (particlesIg003: ext extends base, base restricts anyType).
346
+ #
347
+ # @return [Array<String>]
348
+ def any_type_chain_kinds(type, repository, depth = 0)
349
+ return [] if type.nil? || depth > 10
350
+
351
+ if type.is_a?(Lutaml::Xml::Schema::Xsd::SimpleType)
352
+ return ["restriction"]
353
+ end
354
+
355
+ part = derivation_part(type)
356
+ # A bare complexType (no derivation marker) restricts the
357
+ # ur-type per §3.4.3.
358
+ return ["restriction"] unless part
359
+
360
+ kind = part.class.name.include?("Extension") ? "extension" : "restriction"
361
+ next_local = part.base.to_s.split(":").last
362
+ [kind, *any_type_chain_kinds(
363
+ repository&.resolve_named_type(next_local), repository,
364
+ depth + 1
365
+ )]
265
366
  end
266
367
 
267
368
  # Derivation kind of an override against a type-less
@@ -418,6 +519,96 @@ module Lutaml
418
519
  "#{prefix}:#{local}"
419
520
  end
420
521
 
522
+ # XSD 1.0 §3.3.4 clause 2.2: a builtin override owes valid
523
+ # derivation from the declared type. anyType derives from
524
+ # nothing but itself; every other builtin derives from
525
+ # anySimpleType (§4.1.2 ancestry table); a named declared
526
+ # type must BE the builtin or one of its ancestors
527
+ # (stZ073b: xs:integer is no derivation of the
528
+ # restriction-of-union st.unionType); a declared UNION admits
529
+ # overrides naming a member or a builtin derived from one
530
+ # (the elemT071 rule, builtin members included).
531
+ #
532
+ # @return [Boolean]
533
+ def builtin_override_undervalued?(xml_element, schema_element,
534
+ repository)
535
+ override = xsi_type_name(xml_element).to_s
536
+ declared = declared_local_name(schema_element)
537
+ return false if declared.empty? || declared == override
538
+ return true if override == ANY_TYPE_LOCAL
539
+ return false if declared == ANY_SIMPLE_TYPE_LOCAL
540
+
541
+ covered = ValueSpace.builtin_derivation_cover(declared,
542
+ override)
543
+ return !covered unless covered.nil?
544
+
545
+ declared_type = repository&.resolve_named_type(declared)
546
+ return false unless declared_type.is_a?(
547
+ Lutaml::Xml::Schema::Xsd::SimpleType,
548
+ )
549
+
550
+ if declared_type.union
551
+ return !union_member_covers_builtin?(declared_type, override,
552
+ repository)
553
+ end
554
+
555
+ !validly_derived?(declared_type, override, repository)
556
+ end
557
+
558
+ # Whether the override builtin is admitted by a declared
559
+ # union: it names a member, or a member's chain reaches it.
560
+ #
561
+ # @return [Boolean]
562
+ def union_member_covers_builtin?(union_type, override, repository)
563
+ members = union_type.union.member_types.to_s.split(/\s+/)
564
+ members.any? do |member|
565
+ local = member.to_s.split(":").last
566
+ next true if local == override
567
+
568
+ member_type = repository&.resolve_named_type(local)
569
+ next false unless member_type
570
+
571
+ validly_derived?(member_type, override, repository)
572
+ end
573
+ end
574
+
575
+ # Handles an element whose xsi:type names a built-in: the
576
+ # §3.3.4 derivation gate, then builtin content validation.
577
+ #
578
+ # @return [Boolean] true when the element was handled
579
+ def builtin_override_handled?(xml_element, schema_element,
580
+ collector, repository)
581
+ if builtin_override_undervalued?(xml_element, schema_element,
582
+ repository)
583
+ report_error(
584
+ collector,
585
+ code: "xsi_type_not_derived",
586
+ message: "xsi:type '#{xsi_type_name(xml_element)}' is " \
587
+ "not validly derived from the declared type " \
588
+ "'#{declared_local_name(schema_element)}'",
589
+ location: xml_element.xpath,
590
+ context: { xsi_type: xsi_type_name(xml_element) },
591
+ )
592
+ return true
593
+ end
594
+
595
+ validate_builtin_type(xml_element,
596
+ builtin_override_reference(xml_element),
597
+ collector,
598
+ declared: schema_element,
599
+ repository: repository)
600
+ true
601
+ end
602
+
603
+ # The declared type's local name ('' when the declaration is
604
+ # type-less — the implicit anyType).
605
+ #
606
+ # @param schema_element [Object]
607
+ # @return [String]
608
+ def declared_local_name(schema_element)
609
+ schema_element.type.to_s.split(":").last.to_s
610
+ end
611
+
421
612
  # Resolve the type definition for an element
422
613
  #
423
614
  # @param schema_element [Lutaml::Xsd::Element, Lutaml::Xsd::ComplexType] Schema element
@@ -466,12 +657,17 @@ module Lutaml
466
657
  end
467
658
 
468
659
  # XSD 1.0 §3.3.3: a fixed value constrains the element — a
469
- # non-empty lexical must equal it in the value space
660
+ # non-empty lexical must equal it in the VALUE space
470
661
  # (elemQ018). Empty content already validates against the
471
- # fixed value through effective_text.
662
+ # fixed value through effective_text. Union/list-typed
663
+ # declarations map each lexical through the FIRST accepting
664
+ # member (§4.1.7) — 'true' equals fixed '1' as boolean-true
665
+ # (stE050), token collapse equates trailing blanks
666
+ # (stE065/066), float forms compare numerically (stE055).
472
667
  #
473
668
  # @return [void]
474
- def validate_fixed_content(xml_element, declared, value, collector)
669
+ def validate_fixed_content(xml_element, declared, value, collector,
670
+ repository: nil)
475
671
  return unless declared.is_a?(Lutaml::Xml::Schema::Xsd::Element)
476
672
 
477
673
  fixed = declared.fixed.to_s
@@ -480,6 +676,8 @@ module Lutaml
480
676
 
481
677
  rule = declared_whitespace_rule(declared)
482
678
  return if canonical(value.to_s, rule) == canonical(fixed, rule)
679
+ return if fixed_value_space_equal?(declared, value.to_s, fixed,
680
+ repository)
483
681
 
484
682
  report_error(
485
683
  collector,
@@ -495,6 +693,76 @@ module Lutaml
495
693
  )
496
694
  end
497
695
 
696
+ # Value-space equality for union/list-typed declarations:
697
+ # each side keys through the first accepting member and the
698
+ # keys must match.
699
+ #
700
+ # @return [Boolean]
701
+ def fixed_value_space_equal?(declared, value, fixed, repository)
702
+ simple = declared.simple_type
703
+ return false unless simple
704
+
705
+ if simple.union
706
+ members = simple.union.member_types.to_s.split(/\s+/)
707
+ return union_keys_equal?(value, fixed, members, repository)
708
+ end
709
+ return false unless simple.list
710
+
711
+ item_union = inline_item_union(simple.list, repository)
712
+ return false unless item_union
713
+
714
+ left = value.split
715
+ right = fixed.split
716
+ return false unless left.size == right.size
717
+
718
+ left.zip(right).all? do |a, b|
719
+ union_keys_equal?(a, b, item_union, repository)
720
+ end
721
+ end
722
+
723
+ # The member_types array of a list's item type: an inline
724
+ # union, else the named union resolved through the
725
+ # repository.
726
+ #
727
+ # @return [Array, nil]
728
+ def inline_item_union(list, repository)
729
+ inline = Array(list.simple_type).first
730
+ return inline.union.member_types.to_s.split(/\s+/) if inline&.union
731
+
732
+ reference = list.item_type.to_s.split(":").last
733
+ named = repository&.resolve_named_type(reference)
734
+ return named.union.member_types.to_s.split(/\s+/) if
735
+ named.is_a?(Lutaml::Xml::Schema::Xsd::SimpleType) && named.union
736
+
737
+ nil
738
+ end
739
+
740
+ # @return [Boolean]
741
+ def union_keys_equal?(value, fixed, members, repository)
742
+ a = first_member_key(value, members, repository)
743
+ b = first_member_key(fixed, members, repository)
744
+ !a.nil? && !b.nil? && a == b
745
+ end
746
+
747
+ # @return [Object, nil]
748
+ def first_member_key(lexical, members, repository)
749
+ members.each do |member|
750
+ local = member.to_s.split(":").last
751
+ key = ValueSpace.union_member_value_key(local, lexical)
752
+ return key unless key.nil?
753
+
754
+ named = repository&.resolve_named_type(local)
755
+ next unless named.is_a?(Lutaml::Xml::Schema::Xsd::SimpleType) &&
756
+ named.union
757
+
758
+ nested = first_member_key(lexical,
759
+ named.union.member_types.to_s.split(/\s+/),
760
+ repository)
761
+ return nested unless nested.nil?
762
+ end
763
+ nil
764
+ end
765
+
498
766
  # @return [Symbol] :preserve, :replace or :collapse
499
767
  def declared_whitespace_rule(declared)
500
768
  reference = declared.type.to_s
@@ -543,7 +811,8 @@ module Lutaml
543
811
  collector, repository: nil,
544
812
  declared: nil)
545
813
  value = effective_text(xml_element, declared)
546
- validate_fixed_content(xml_element, declared, value, collector)
814
+ validate_fixed_content(xml_element, declared, value, collector,
815
+ repository: repository)
547
816
 
548
817
  # Validate base type if present
549
818
  if simple_type.restriction
@@ -566,7 +835,8 @@ module Lutaml
566
835
  # @param collector [ResultCollector] Result collector
567
836
  # @return [void]
568
837
  def validate_complex_type_content(xml_element, complex_type,
569
- collector, repository: nil)
838
+ collector, repository: nil,
839
+ declared: nil)
570
840
  # Complex type validation is handled by:
571
841
  # - AttributeValidationRule for attributes
572
842
  # - ContentModelValidationRule for content model
@@ -575,7 +845,8 @@ module Lutaml
575
845
  # Check for simple content
576
846
  if complex_type.simple_content
577
847
  validate_simple_content(xml_element, complex_type.simple_content,
578
- collector, repository: repository)
848
+ collector, repository: repository,
849
+ declared: declared)
579
850
  end
580
851
  end
581
852
 
@@ -1044,12 +1315,27 @@ module Lutaml
1044
1315
  # @param collector [ResultCollector] Result collector
1045
1316
  # @return [void]
1046
1317
  def validate_simple_content(xml_element, simple_content, collector,
1047
- repository: nil)
1318
+ repository: nil, declared: nil)
1048
1319
  # Simple content means element has text content and attributes
1049
1320
  # Validate the text content as a simple type
1050
1321
  if simple_content.extension
1051
- # Extension adds attributes to a simple type
1052
- # Text content validation handled by base type
1322
+ # The extension's TEXT still validates against the
1323
+ # extension's base type — the extension itself only adds
1324
+ # attributes (addB114: '62' violates base A's enumeration
1325
+ # and range through E-A's simpleContent extension).
1326
+ raw_base = simple_content.extension.base.to_s
1327
+ base_local = raw_base.split(":").last
1328
+ base_type = repository&.resolve_named_type(base_local)
1329
+ if base_type.is_a?(Lutaml::Xml::Schema::Xsd::SimpleType)
1330
+ validate_simple_type_content(xml_element, base_type,
1331
+ collector,
1332
+ repository: repository,
1333
+ declared: declared)
1334
+ elsif builtin_base?(raw_base)
1335
+ validate_builtin_type(xml_element, raw_base, collector,
1336
+ declared: declared,
1337
+ repository: repository)
1338
+ end
1053
1339
  elsif simple_content.restriction
1054
1340
  # Restriction constrains a simple type
1055
1341
  value = xml_element.text_content
@@ -1070,9 +1356,10 @@ module Lutaml
1070
1356
  # @param collector [ResultCollector] Result collector
1071
1357
  # @return [void]
1072
1358
  def validate_builtin_type(xml_element, type_name, collector,
1073
- declared: nil)
1359
+ declared: nil, repository: nil)
1074
1360
  value = effective_text(xml_element, declared)
1075
- validate_fixed_content(xml_element, declared, value, collector)
1361
+ validate_fixed_content(xml_element, declared, value, collector,
1362
+ repository: repository)
1076
1363
  # A simple type has no element content: children under a
1077
1364
  # builtin-typed element are illegal even when the lexical
1078
1365
  # text is fine (dtZ86723 — the xsi:type-overridden root
@@ -66,6 +66,144 @@ 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
+ # The VALUE-SPACE key of a lexical under a union member
124
+ # builtin — nil when the member does not accept the lexical.
125
+ # A union maps a lexical through its FIRST accepting member
126
+ # (§4.1.7), so 'true' and '1' are both boolean-true (stE050)
127
+ # and token collapse equates trailing blanks (stE065/066).
128
+ #
129
+ # @param local [String] builtin member local name
130
+ # @param value [String] the lexical
131
+ # @return [Object, nil] a comparable value, or nil
132
+ def self.union_member_value_key(local, value)
133
+ text = value.to_s
134
+ case local
135
+ when "boolean"
136
+ b = text.strip
137
+ return nil unless b.match?(/\A(?:true|false|1|0)\z/)
138
+
139
+ %w[true 1].include?(b)
140
+ when *INTEGER_TYPES
141
+ return nil unless integer_valid?(local, text)
142
+
143
+ Integer(text.strip)
144
+ when "float", "double"
145
+ return nil unless text.strip.match?(
146
+ /\A(?:[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?|INF|-INF|NaN)\z/,
147
+ )
148
+
149
+ f = Float(text.strip)
150
+ f.nan? ? "nan" : f
151
+ when "decimal"
152
+ return nil unless text.strip.match?(
153
+ /\A[+-]?(?:\d+(?:\.\d*)?|\.\d+)\z/,
154
+ )
155
+
156
+ require "bigdecimal"
157
+ BigDecimal(text.strip)
158
+ when "string"
159
+ text
160
+ when "normalizedString"
161
+ text.gsub(/[\t\n\r]/, " ")
162
+ when *COLLAPSED_STRING_TYPES, "language", "Name", "NCName",
163
+ "ID", "IDREF", "NMTOKEN", "anyURI"
164
+ text.strip.split(/\s+/).join(" ")
165
+ when "gYear", "gYearMonth", "gMonth", "gMonthDay", "gDay"
166
+ return nil unless calendar_lexical_valid?(local, text)
167
+
168
+ calendar_key(text)
169
+ when "date", "dateTime", "time"
170
+ return nil unless lexical_answer(local, text) == true
171
+
172
+ temporal_key(text)
173
+ when "duration"
174
+ return nil unless duration_valid?(text)
175
+
176
+ duration_key(text)
177
+ when "hexBinary"
178
+ return nil unless text.match?(/\A(?:[0-9a-fA-F]{2})*\z/)
179
+
180
+ text.upcase
181
+ when "base64Binary"
182
+ return nil unless base64_valid?(text)
183
+
184
+ text
185
+ end
186
+ end
187
+
188
+ # Whether a builtin override local is validly derived from the
189
+ # declared local: identity or the declared name sits in the
190
+ # override's builtin ancestry. TRI-STATE: nil when the override
191
+ # is not a builtin local at all (the caller falls through to
192
+ # schema-type resolution). No `?` suffix — rubocop -A rewrites
193
+ # nil-returning predicate guards to false and breaks the
194
+ # contract.
195
+ #
196
+ # @param declared [String] declared type local name
197
+ # @param override [String] override builtin local name
198
+ # @return [Boolean, nil]
199
+ def self.builtin_derivation_cover(declared, override)
200
+ ancestors = BUILTIN_ANCESTORS[override.to_s]
201
+ return nil unless ancestors
202
+
203
+ declared.to_s == override.to_s ||
204
+ ancestors.include?(declared.to_s)
205
+ end
206
+
69
207
  # xsd:length counts list ITEMS for list types, not characters.
70
208
  LIST_BASES = %w[IDREFS ENTITIES NMTOKENS].freeze
71
209
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Xsd
5
- VERSION = "1.2.113"
5
+ VERSION = "1.2.115"
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.113
4
+ version: 1.2.115
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.