lutaml-xsd 1.2.79 → 1.2.81

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: fc71d86940b282e842650b924ddfbef4c92dbb7d39645dec6e57cf80c5edbd81
4
- data.tar.gz: e7643e86eb653d99ac2a72c39792b828994c1203a0fbbc89042856456ba59855
3
+ metadata.gz: 467b4c2343707af1d200289a1666d12e3ab984c438f0a29ef601d92143914562
4
+ data.tar.gz: d249de5b32c7fb201c8025cf56af088bde07acb6e1f76d2cd3ae1f15bd39cf29
5
5
  SHA512:
6
- metadata.gz: 65b97ebeb74d183e3347e801b4d67fc183dec70698e501dd60fffcedcafbcc9a33f97d830aab3449a9ab72c8ca5ab0699aa41a5aafbbbd731512134408afcade
7
- data.tar.gz: a4ae99c07f652e26102f6bae1f6a3d1c241d032436e2c424f4e03f06921b6feb0ef46798febf19598ec005fbbf6757829638c543e557ab399e6c2ffb785264b2
6
+ metadata.gz: 0f9c9c9c71bf3e064dd93ab6e007df3f9d90aaad2478bf77d8f7b7915804b5e8218a6d8adb97be02293560dc261ed957a6fe09af5404a30b18ee7a0d5d1a941e
7
+ data.tar.gz: 539a1503358510cde20356dc3d6a0a1a2c0b9daaebf77bda9ef45bc54f45222a0eaf2d97477b05f1347b1d35dcc22742963b0fc388823217bc6cd45ceb900bdf
data/TODO.conformance.md CHANGED
@@ -120,22 +120,29 @@ pinned commit) is measured via `rake conformance:w3c`:
120
120
 
121
121
  | mode | pass-rate |
122
122
  |---|---|
123
- | pipeline | 4569/4696 (97.3%) |
123
+ | pipeline | 4573/4696 (97.38%) |
124
124
 
125
- (2026-11-02, bundled-stack measurement on main with the escape,
125
+ (2026-11-02, bundled-stack measurement with the escape,
126
126
  hint-following, pattern-alternative, identity-crash, engine-set,
127
127
  engine-refresh, IC value-space, IC field-resolution, facade-silence,
128
128
  regex-semantics, redefine-refresh, gmonth-refresh, digit-vintage,
129
- stgh-refresh, group-ref, attribute-group-redefine and
130
- notation-length rounds; moxml
131
- 0.6.1 (gemspec floor `~> 0.6.0` per the owner — the 0.6 line drops
132
- the Opal surface; measured conformance-neutral), lutaml-model
133
- 0.8.155, engine leptris 1.9.401.0.) Same-stack comparison: 1.2.43
129
+ stgh-refresh, group-ref, attribute-group-redefine,
130
+ notation-length and ref-namespace rounds; moxml
131
+ 0.6.2 (gemspec floor `~> 0.6.0` per the owner — the 0.6 line drops
132
+ the Opal surface), lutaml-model
133
+ 0.8.157 (gemspec floor `~> 0.8.156` per the owner; the lockfile is
134
+ no longer committed — measurements resolve fresh), engine leptris
135
+ 1.9.402.0. The 1.9.402/0.6.2/0.8.157 ride costs 8 net: engine
136
+ fixed+EMPTY content regressions (9 cases, filed leptris-rs#25 —
137
+ empty content satisfies the fixed facet per §3.12.2/§4.3.1.2),
138
+ reU6 we-accept, MINUS two drift fixes (RegexTest_234, stE063).)
139
+ Same-stack comparison: 1.2.43
134
140
  baseline 3961 (84.3%) → 4039 (86.0%) → 4090 (87.1%) → 4360 (92.8%,
135
141
  lockfile refresh) → 4411 (93.9%) → 4430 (94.3%) → 4435 (94.4%) →
136
142
  4436 (94.5%) → 4439 (94.5%) → 4454 (94.9%) → 4495 (95.7%) → 4524
137
143
  (96.3%) → 4534 (96.6%) → 4540 (96.7%) → 4542 (96.7%) → 4546 (96.8%)
138
- → 4557 (97.0%) → 4565 (97.2%) → 4569 (97.3%), +608 net over the 1.2.43 baseline. The escapes round (PR #197 — \d→\p{Nd},
144
+ → 4557 (97.0%) → 4565 (97.2%) → 4569 (97.3%) → 4562 (drift raw) →
145
+ 4563 (97.17%, ref-namespace round) → 4573 (97.38%, group-content round), +612 net over the 1.2.43 baseline. The escapes round (PR #197 — \d→\p{Nd},
139
146
  \w/\W/\s to their XSD value spaces) collapses Regex 120→69;
140
147
  same-step pattern facets now behave as alternatives and
141
148
  whitespace-only text reaches the facets intact (PR #200), Regex
@@ -230,6 +237,8 @@ corners.
230
237
  | Built-in lexical tables reject valid forms (char-reference shapes; gMonth FIXED 1.9.398, \d legacy-Nd FIXED 1.9.399, stG/stH list lexicals FIXED 1.9.400/401) | ~5 | leptris#1936 |
231
238
  | Automaton size cap surfaces as an instance error instead of engine-inert silence | 0 — FIXED in leptris 1.9.398 | moxml#514 (follow-up to #473) |
232
239
  | Regex dialect remainder (engine `\d` vintage, `\p{Is...}` blocks, char-reference shapes) | ~30 | engine-side of leptris#1936 |
240
+ | Empty content against a fixed element value rejected (1.9.402.0 regression — isDefault001/002/076, ctZ013a, elemQ019, stE065/066/096, particlesZ040) | 9 | leptris-rs#25 |
241
+ | Unresolvable include/import/redefine schemaLocation raises at parse instead of warning (anyURI_a001/a002, schD7, schG8) | 4 | lutaml-model#1087 |
233
242
  | Strict wildcard/attribute admission on further compositions | ~5 | moxml#510 thread |
234
243
  | Ruby-side under-detections (Group 16, DataTypes 7, IC 6, Attribute 5, Additional 4, Particles 4, SimpleType 4, Element 1, Wildcards 1 — minus what the regex round banked) | ~60 | OURS — the remaining Ruby work queue |
235
244
 
@@ -388,6 +397,59 @@ tuple keys, node-set field violations and absent-field rows handled, cross-eleme
388
397
  duration/dateTime/gMonthDay/gYear corners (engine-bound and
389
398
  contested-highlight), gMonth_pattern001 (bugzilla 6901 — do not
390
399
  chase), double018 (disputed reading).
400
+ - REF-NAMESPACE ROUND (2026-11-02): +2 Particles (13→11, particlesDb001/
401
+ Db006). A ref particle names a declaration in ONE namespace, but the
402
+ match path resolved it against the CHILD's namespace first — a global
403
+ declaration duplicated across an imported namespace satisfied the ref
404
+ and foreign substitutes were accepted. The ref branch now prefers the
405
+ declaration in the content-model OWNER schema's target namespace
406
+ (threaded through the match chain), then the child's, then the
407
+ unique-name fallback; parsed particles carry no prefix map
408
+ (lutaml-model discards xmlns), so the owner namespace is the
409
+ strongest signal available. The engine — fed documents one at a
410
+ time — admits the substitute, so a targeted scan on the engine-accept
411
+ path reports the foreign child (scan_cross_namespace_refs). NIL
412
+ owner namespace means UNKNOWN, not "no namespace": resolve(local,
413
+ nil) only matches no-namespace declarations and a hint-loaded
414
+ document can supply a foreign namesake (idH021 regression caught
415
+ and fixed in-round). Zero regressions on the drifted stack.
416
+ - STACK DRIFT (2026-11-02, same day): the lockfile is no longer
417
+ committed (owner chore) — measurements resolve fresh and rode
418
+ moxml 0.6.1→0.6.2, leptris 1.9.401.0→1.9.402.0, lutaml-model
419
+ 0.8.155→0.8.157 at once. TWO fixes rode in (RegexTest_234,
420
+ stE063); the fixed+EMPTY engine regression cost 9 (leptris-rs#25)
421
+ and reU6 flipped to we-accept (regex class, untriaged). Suite
422
+ 4569→4562 raw; 4563 with this round. NET -6 documented, not
423
+ chased — the fixed-empty leg reverses the moment leptris-rs#25
424
+ lands.
425
+ - GROUP-CONTENT ROUND (2026-11-02): +13 (Group 8→0 — the family is
426
+ CLEAN at 126/126; Particles Db001/Db006 +2, Ea004/011/018 +3; suite
427
+ 4563→4573 = 97.38% on the drifted stack, rspec green). FOUR Ruby
428
+ defects: (a) resolve_group scanned only top-level schemas — a group
429
+ declared in an IMPORTED document (the extension-body shape)
430
+ resolved to nothing (groupB009/010); nested resolved schemas are
431
+ now searched depth-guarded. (b) The NFA wiring missed the
432
+ lutaml-model container classes: a group whose declaration is xs:all
433
+ compiled ORDERED (groupO004), and with_occurrence gave min=0
434
+ particles one phantom extra copy (min=0/max=1 compiled capacity 2 —
435
+ masked for bare elements by the occurrence rule, visible wherever
436
+ nested group bounds lean on this verdict: groupJ003/J006/J008/L008
437
+ accepted forbidden repeats); a max=0 particle compiles to the empty
438
+ fragment (groupH008). (c) type_particles had no complexContent
439
+ restriction branch — a restriction body compiled EMPTY and children
440
+ passed unchecked (groupH008); a restriction replaces the base
441
+ content model. (d) The ref owner-namespace probe resolved ANY
442
+ declaration — including inline ones — so a qualified local namesake
443
+ hijacked unprefixed refs; the probe is GLOBAL-only (resolve_global),
444
+ the unprefixed ref canonically names a no-namespace declaration,
445
+ several distinct prefixed forms of one local name mean deliberate
446
+ disambiguation (no owner guess), and an unresolvable ref matches by
447
+ local name alone. KNOWN GAP left open: particlesQ032 — a
448
+ no-namespace document imported under an EXPLICIT namespace: its
449
+ globals must adopt the import namespace (MS-lenient chameleon
450
+ import; our index keeps them no-namespace so the four e2 refs
451
+ mis-resolve). Zero regressions; the drift entries above remain
452
+ (fixed-empty 9, reU6).
391
453
  - REGEX SEMANTICS ROUND (2026-10-31, PR #208): \w/\W now carry their
392
454
  Appendix F complements (\w = [^\p{P}\p{Z}\p{C}], class interiors
393
455
  via the exact positive partition L/M/N/S), \I/\C complements added
@@ -134,13 +134,13 @@ module Lutaml
134
134
 
135
135
  def compile_particle(particle)
136
136
  case particle
137
- when Lutaml::Xsd::Sequence
137
+ when Lutaml::Xsd::Sequence, Lutaml::Xml::Schema::Xsd::Sequence
138
138
  compile_bounded_container(particle)
139
- when Lutaml::Xsd::Choice
139
+ when Lutaml::Xsd::Choice, Lutaml::Xml::Schema::Xsd::Choice
140
140
  with_occurrence(particle) do
141
141
  compile_choice_particles(@container_particles.call(particle))
142
142
  end
143
- when Lutaml::Xsd::All
143
+ when Lutaml::Xsd::All, Lutaml::Xml::Schema::Xsd::All
144
144
  # The all's own bounds wrap the member bitmask: an optional
145
145
  # all accepts the empty sequence even with mandatory
146
146
  # members (addB31).
@@ -181,10 +181,17 @@ module Lutaml
181
181
  end
182
182
 
183
183
  # A group ref expands to its declaration's container, whose own
184
- # kind (sequence vs choice) decides the wiring.
184
+ # kind (sequence vs choice vs all) decides the wiring. Imported
185
+ # documents parse to the lutaml-model container classes —
186
+ # groupO004's xs:all compiled ORDERED before both All classes
187
+ # were wired to the bitmask.
185
188
  def compile_container_body(container)
186
- if container.is_a?(Lutaml::Xsd::Choice)
189
+ if container.is_a?(Lutaml::Xsd::Choice) ||
190
+ container.is_a?(Lutaml::Xml::Schema::Xsd::Choice)
187
191
  compile_choice_particles(@container_particles.call(container))
192
+ elsif container.is_a?(Lutaml::Xsd::All) ||
193
+ container.is_a?(Lutaml::Xml::Schema::Xsd::All)
194
+ with_occurrence(container) { compile_all(container) }
188
195
  else
189
196
  compile_particles(@container_particles.call(container))
190
197
  end
@@ -235,14 +242,20 @@ module Lutaml
235
242
 
236
243
  # Wraps a body fragment with the particle's occurrence range:
237
244
  # min mandatory copies, then an unbounded loop on one extra copy
238
- # or (max - min) optional copies.
245
+ # or (max - min) optional copies. The first body copy counts
246
+ # toward the bound: with min=0 it IS the first optional copy, so
247
+ # only max-1 more are wired (min=0/max=1 used to compile with
248
+ # capacity 2 — masked for bare elements by the occurrence rule,
249
+ # visible wherever nested group bounds lean on this verdict).
239
250
  def with_occurrence(particle)
240
251
  min = parse_occurs(particle.min_occurs, 1)
241
252
  max = parse_max_occurs(particle.max_occurs, 1)
242
- eff_min = [min, MAX_UNROLL].min
253
+ return empty_fragment if max.is_a?(Integer) && max.zero?
254
+
243
255
  entry, exit_state = yield
244
256
  cursor = exit_state
245
257
  last_entry = entry
258
+ eff_min = [min, MAX_UNROLL].min
246
259
  (eff_min - 1).times do
247
260
  c_entry, c_exit = yield
248
261
  @epsilons[cursor] << c_entry
@@ -259,7 +272,8 @@ module Lutaml
259
272
  @epsilons[exit_state] << final
260
273
  else
261
274
  @epsilons[cursor] << final
262
- (max - min).times do
275
+ optional = max - min - (min.zero? && max.is_a?(Integer) ? 1 : 0)
276
+ optional.times do
263
277
  c_entry, c_exit = yield
264
278
  @epsilons[cursor] << c_entry
265
279
  @epsilons[c_exit] << final
@@ -94,8 +94,9 @@ module Lutaml
94
94
 
95
95
  # The engine does not check abstract-element usage; scan for
96
96
  # it regardless of whether the engine answered.
97
+ owner_ns = repository&.target_namespace_for(schema_def)
97
98
  scan_abstract_usage(xml_element, particles, collector,
98
- repository)
99
+ repository, owner_ns)
99
100
 
100
101
  handled = if occurrence_guard(repository).particles_safe?(particles)
101
102
  engine_content_check(xml_element,
@@ -107,10 +108,14 @@ module Lutaml
107
108
  # (particlesZ034_b); the Ruby matcher decides those.
108
109
  false
109
110
  end
110
- return if handled
111
+ if handled
112
+ scan_cross_namespace_refs(xml_element, particles, collector,
113
+ repository, owner_ns)
114
+ return
115
+ end
111
116
 
112
117
  validate_particle_sequence(xml_element, particles, collector,
113
- repository)
118
+ repository, owner_ns)
114
119
  end
115
120
 
116
121
  private
@@ -133,14 +138,14 @@ module Lutaml
133
138
  #
134
139
  # @return [void]
135
140
  def validate_particle_sequence(xml_element, particles, collector,
136
- repository)
141
+ repository, owner_ns = nil)
137
142
  return if particles.empty?
138
143
 
139
144
  children = Array(xml_element.children)
140
145
  matcher = ContentModelMatcher.new(
141
146
  particles,
142
147
  element_match: ->(child, particle) do
143
- element_matches?(child, particle, repository)
148
+ element_matches?(child, particle, repository, owner_ns)
144
149
  end,
145
150
  wildcard_match: ->(child, wildcard) do
146
151
  wildcard_permits?(wildcard, xml_element, child, repository)
@@ -155,13 +160,13 @@ module Lutaml
155
160
  result = matcher.match(children)
156
161
  if result.matched
157
162
  scan_wildcard_strict_usage(xml_element, particles, collector,
158
- repository)
163
+ repository, owner_ns)
159
164
  return
160
165
  end
161
166
 
162
167
  known = collector.errors.size
163
168
  walk_sequence_diagnostics(xml_element, particles, collector,
164
- repository)
169
+ repository, owner_ns)
165
170
  return if collector.errors.size > known
166
171
 
167
172
  report_matcher_failure(result, children, xml_element, collector)
@@ -294,6 +299,7 @@ module Lutaml
294
299
  type.complex_content
295
300
  content = type.complex_content
296
301
  extension = Array(content.extension).first
302
+ restriction = Array(content.restriction).first
297
303
  if extension
298
304
  base_local = extension.base.to_s.split(":").last
299
305
  base_type = base_local && repository&.resolve_named_type(base_local)
@@ -306,6 +312,14 @@ module Lutaml
306
312
  particles << particle
307
313
  end
308
314
  end
315
+ elsif restriction
316
+ # A restriction REPLACES the base content model: only
317
+ # the restriction body's particles apply (groupH008's
318
+ # prohibited group ref was previously compiled as an
319
+ # EMPTY model and the children passed unchecked).
320
+ particles = COMPLEX_PARTICLE_KINDS.flat_map do |kind|
321
+ Array(restriction.public_send(kind)).compact
322
+ end
309
323
  end
310
324
  end
311
325
 
@@ -317,15 +331,16 @@ module Lutaml
317
331
  # instances. Containers flatten to their leaf declarations —
318
332
  # the walk keeps them whole, the scan needs the leaves.
319
333
  def scan_abstract_usage(xml_element, particles, collector,
320
- repository)
334
+ repository, owner_ns = nil)
321
335
  leaves = leaf_particles(particles, repository)
322
336
  xml_element.children.each do |child|
323
337
  leaves.each do |particle|
324
338
  next unless particle.is_a?(Lutaml::Xsd::Element)
325
339
  next unless particle_matches?(child, particle, repository,
326
- parent: xml_element)
340
+ parent: xml_element,
341
+ owner_ns: owner_ns)
327
342
  next unless element_declaration_abstract?(particle, child,
328
- repository)
343
+ repository, owner_ns)
329
344
 
330
345
  report_error(
331
346
  collector,
@@ -360,7 +375,7 @@ module Lutaml
360
375
  #
361
376
  # @return [void]
362
377
  def scan_wildcard_strict_usage(xml_element, particles, collector,
363
- repository)
378
+ repository, owner_ns = nil)
364
379
  leaves = leaf_particles(particles, repository)
365
380
  wildcards = leaves.select { |p| p.is_a?(Lutaml::Xsd::Any) }
366
381
  return if wildcards.empty?
@@ -370,7 +385,7 @@ module Lutaml
370
385
  end
371
386
  xml_element.children.each do |child|
372
387
  next if element_leaves.any? do |p|
373
- element_matches?(child, p, repository)
388
+ element_matches?(child, p, repository, owner_ns)
374
389
  end
375
390
 
376
391
  wildcards.each do |wildcard|
@@ -391,7 +406,7 @@ module Lutaml
391
406
  #
392
407
  # @return [void]
393
408
  def walk_sequence_diagnostics(xml_element, particles, collector,
394
- repository)
409
+ repository, owner_ns = nil)
395
410
  return if particles.empty?
396
411
 
397
412
  children = xml_element.children
@@ -418,13 +433,15 @@ module Lutaml
418
433
  child = children[child_index]
419
434
 
420
435
  break unless particle_matches?(child, particle, repository,
421
- parent: xml_element)
436
+ parent: xml_element,
437
+ owner_ns: owner_ns)
422
438
 
423
439
  if particle.is_a?(Lutaml::Xsd::Any)
424
440
  check_strict_wildcard_child(child, particle, collector,
425
441
  repository)
426
442
  elsif particle.is_a?(Lutaml::Xsd::Element) &&
427
- element_declaration_abstract?(particle, child, repository)
443
+ element_declaration_abstract?(particle, child, repository,
444
+ owner_ns)
428
445
  report_error(
429
446
  collector,
430
447
  code: "abstract_element_used",
@@ -489,19 +506,101 @@ module Lutaml
489
506
  # declarations cannot appear directly in instances (substitution
490
507
  # members remain legal). Direct declarations carry their own
491
508
  # abstract flag; references resolve to the global declaration.
492
- def element_declaration_abstract?(particle, child, repository)
493
- declaration = declaration_for(particle, child, repository)
509
+ def element_declaration_abstract?(particle, child, repository,
510
+ owner_ns = nil)
511
+ declaration = declaration_for(particle, child, repository,
512
+ owner_ns)
494
513
  return false unless declaration
495
514
 
496
515
  truthy?(declaration.abstract) && child.name == declaration.name
497
516
  end
498
517
 
499
- def declaration_for(particle, child, repository)
518
+ # A qualified ref names a declaration in ONE namespace.
519
+ # Global names duplicated across namespaces (import + main)
520
+ # resolve against the child's namespace today; a foreign
521
+ # child is an unexpected element (the engine — fed the
522
+ # documents one at a time — admits the substitute, so this
523
+ # runs on the engine-accept path). Wildcard territory is
524
+ # exempt. The PREFIX is the signal: an unprefixed ref
525
+ # canonically names a NO-NAMESPACE declaration (Q016 — its
526
+ # qualified local namesake is irrelevant), a prefixed one
527
+ # names some namespace: a child in the opposite territory,
528
+ # or — for the duplicated-name shape — in any namespace but
529
+ # the owner's, cannot match.
530
+ #
531
+ # @return [void]
532
+ def scan_cross_namespace_refs(xml_element, particles, collector,
533
+ repository, owner_ns)
534
+ return if owner_ns.nil?
535
+
536
+ refs = leaf_particles(particles, repository).select do |p|
537
+ p.is_a?(Lutaml::Xml::Schema::Xsd::Element) &&
538
+ p.ref.to_s.match?(/\S/)
539
+ end
540
+ return if refs.empty?
541
+
542
+ # Several DISTINCT prefixed forms of one local name mean the
543
+ # schema disambiguates by prefix on purpose (particlesQ032:
544
+ # foo:e2 / x:e2 / bar:e2) — the owner-namespace guess is
545
+ # untenable there.
546
+ single_prefixed_form = refs.select do |r|
547
+ r.ref.to_s.include?(":")
548
+ end.group_by { |r| r.ref.to_s.split(":").last }
549
+ .select { |_, group| group.map { |r| r.ref.to_s.split(":").first }.uniq.size == 1 }
550
+ .map(&:first)
551
+
552
+ xml_element.children.each do |child|
553
+ child_ns = child.namespace_uri.to_s
554
+ next if child_ns == owner_ns
555
+
556
+ hit = refs.find do |r|
557
+ ref = r.ref.to_s
558
+ next false unless ref.split(":").last == child.name
559
+
560
+ if ref.include?(":")
561
+ next false unless single_prefixed_form.include?(child.name)
562
+
563
+ child_ns.empty? ||
564
+ (child_ns != owner_ns &&
565
+ repository.resolve_global(child.name, owner_ns))
566
+ else
567
+ child_ns.match?(/\S/)
568
+ end
569
+ end
570
+ next unless hit
571
+ next if wildcard_covers_child?(xml_element, particles, child,
572
+ repository)
573
+
574
+ report_error(
575
+ collector,
576
+ code: "unexpected_element_in_sequence",
577
+ message: "Unexpected element '#{child.qualified_name}' " \
578
+ "in sequence — the reference names the same " \
579
+ "declaration in another namespace",
580
+ location: child.xpath,
581
+ context: { element: child.qualified_name },
582
+ suggestion: "Use the declaration's namespace or rename " \
583
+ "the duplicate",
584
+ )
585
+ end
586
+ end
587
+
588
+ def wildcard_covers_child?(xml_element, particles, child,
589
+ repository)
590
+ leaf_particles(particles, repository).any? do |p|
591
+ next false unless p.is_a?(Lutaml::Xsd::Any)
592
+
593
+ wildcard_permits?(p, xml_element, child, repository)
594
+ end
595
+ end
596
+
597
+ def declaration_for(particle, child, repository, owner_ns = nil)
500
598
  return particle if particle.name.to_s.match?(/\S/)
501
599
  return nil unless repository
502
600
 
503
601
  local = particle.ref.to_s.split(":").last
504
- repository.resolve(local, child.namespace_uri) ||
602
+ (owner_ns && repository.resolve_global(local, owner_ns)) ||
603
+ repository.resolve(local, child.namespace_uri) ||
505
604
  repository.resolve(local, nil)
506
605
  end
507
606
 
@@ -649,10 +748,11 @@ module Lutaml
649
748
  # wildcard namespace constraints scoped to the target namespace
650
749
  # @return [Boolean]
651
750
  def particle_matches?(xml_element, particle, repository = nil,
652
- parent: nil, group_depth: 0)
751
+ parent: nil, group_depth: 0, owner_ns: nil)
653
752
  particle_matches_child?(xml_element, particle, repository,
654
753
  parent: parent,
655
- group_depth: group_depth)
754
+ group_depth: group_depth,
755
+ owner_ns: owner_ns)
656
756
  end
657
757
 
658
758
  # Check if XML element matches schema element.
@@ -663,9 +763,10 @@ module Lutaml
663
763
  # @param schema_element [Lutaml::Xsd::Element] The schema element
664
764
  # @param repository [SchemaDefinitionResolver, nil]
665
765
  # @return [Boolean]
666
- def element_matches?(xml_element, schema_element, repository = nil)
766
+ def element_matches?(xml_element, schema_element, repository = nil,
767
+ owner_ns = nil)
667
768
  particle_matches_declaration?(xml_element, schema_element,
668
- repository) ||
769
+ repository, owner_ns) ||
669
770
  substitution_member?(xml_element, schema_element, repository)
670
771
  end
671
772
 
@@ -156,6 +156,27 @@ module Lutaml
156
156
  candidates.length == 1 ? candidates.first.first : nil
157
157
  end
158
158
 
159
+ # Global (top-level) element declaration only. A ref particle
160
+ # names a GLOBAL declaration; the index also holds inline ones,
161
+ # and a qualified local namesake must never satisfy the
162
+ # owner-namespace probe (particlesQ016: ref "foo" unprefixed vs
163
+ # the qualified local foo inside the restricted type).
164
+ #
165
+ # @param local_name [String]
166
+ # @param namespace_uri [String, nil]
167
+ # @return [Lutaml::Xml::Schema::Xsd::Element, nil]
168
+ def resolve_global(local_name, namespace_uri)
169
+ return nil if local_name.nil? || local_name.empty?
170
+
171
+ candidates = @elements[local_name]
172
+ return nil if candidates.empty?
173
+
174
+ exact = candidates.find do |_element, namespace, kind|
175
+ kind == :top && namespace_matches?(namespace, namespace_uri)
176
+ end
177
+ exact&.first
178
+ end
179
+
159
180
  # Named type for an element's `type="prefix:Name"` reference.
160
181
  #
161
182
  # @param schema_element [Lutaml::Xml::Schema::Xsd::Element, nil]
@@ -386,7 +407,10 @@ module Lutaml
386
407
  end
387
408
 
388
409
  # Named group declaration by local name (for xs:group reference
389
- # expansion in the content and occurrence walks).
410
+ # expansion in the content and occurrence walks). Group refs
411
+ # reach across imports (groupB009: an extension body references
412
+ # a group declared in the imported document), so nested
413
+ # resolved schemas are searched too.
390
414
  #
391
415
  # @param local_name [String]
392
416
  # @return [Lutaml::Xml::Schema::Xsd::Group, nil]
@@ -396,6 +420,25 @@ module Lutaml
396
420
  schemas.each do |schema|
397
421
  found = Array(schema.group).find { |g| g.name == local_name }
398
422
  return found if found
423
+
424
+ found = find_nested_group(local_name, schema)
425
+ return found if found
426
+ end
427
+ nil
428
+ end
429
+
430
+ # @return [Object, nil]
431
+ def find_nested_group(local_name, schema, depth = 0)
432
+ return nil if depth > 8
433
+
434
+ (Array(schema.include) + Array(schema.import)).each do |nested|
435
+ next unless nested.is_a?(Lutaml::Xml::Schema::Xsd::Schema)
436
+
437
+ found = Array(nested.group).find { |g| g.name == local_name }
438
+ return found if found
439
+
440
+ found = find_nested_group(local_name, nested, depth + 1)
441
+ return found if found
399
442
  end
400
443
  nil
401
444
  end
@@ -227,14 +227,18 @@ module Lutaml
227
227
 
228
228
  # Whether an instance child matches a content-model particle.
229
229
  # Direct declarations compare name and expected namespace; ref
230
- # particles take the name and namespace of the global declaration
231
- # they reference, resolved against the child's namespace.
230
+ # particles name a qualified declaration — the owner schema's
231
+ # target namespace is preferred, then the child's, then the
232
+ # unique-name fallback (parsed particles carry no prefix map).
232
233
  #
233
234
  # @param child [XmlElement]
234
235
  # @param particle [Lutaml::Xsd::Element]
235
236
  # @param repository [SchemaDefinitionResolver, nil]
237
+ # @param owner_ns [String, nil] target namespace of the schema
238
+ # owning the content model
236
239
  # @return [Boolean]
237
- def particle_matches_declaration?(child, particle, repository)
240
+ def particle_matches_declaration?(child, particle, repository,
241
+ owner_ns = nil)
238
242
  ref = particle.ref.to_s
239
243
  if ref.empty?
240
244
  name = particle.name.to_s
@@ -245,9 +249,20 @@ module Lutaml
245
249
  repository))
246
250
  else
247
251
  local = ref.split(":").last
248
- declaration = repository&.resolve(local, child.namespace_uri) ||
252
+ # owner_ns nil means UNKNOWN (the caller has no owner
253
+ # context), not "no namespace" — resolve(local, nil) only
254
+ # matches no-namespace declarations, and a hint-loaded
255
+ # document can supply a foreign namesake. The owner probe
256
+ # is GLOBAL-only: a ref names a global declaration, and an
257
+ # inline namesake must not satisfy it.
258
+ declaration =
259
+ (owner_ns && repository&.resolve_global(local, owner_ns)) ||
260
+ repository&.resolve(local, child.namespace_uri) ||
249
261
  repository&.resolve(local, nil)
250
- return false unless declaration
262
+ # An unresolvable ref (particlesQ032: a no-namespace document
263
+ # imported under an explicit namespace — its globals index
264
+ # nowhere) is namespace-unconstrained: match by local name.
265
+ return child.name == local unless declaration
251
266
 
252
267
  child.name == declaration.name &&
253
268
  namespaces_match?(child.namespace_uri,
@@ -413,10 +428,12 @@ module Lutaml
413
428
  # @param group_depth [Integer] recursion guard for group refs
414
429
  # @return [Boolean]
415
430
  def particle_matches_child?(child, particle, repository,
416
- parent: nil, group_depth: 0)
431
+ parent: nil, group_depth: 0,
432
+ owner_ns: nil)
417
433
  case particle
418
434
  when Lutaml::Xsd::Element
419
- particle_matches_declaration?(child, particle, repository) ||
435
+ particle_matches_declaration?(child, particle, repository,
436
+ owner_ns) ||
420
437
  substitution_member?(child, particle, repository)
421
438
  when Lutaml::Xsd::Any
422
439
  wildcard_permits?(particle, parent || child, child, repository)
@@ -425,13 +442,15 @@ module Lutaml
425
442
  group_depth).any? do |container|
426
443
  particle_matches_child?(child, container, repository,
427
444
  parent: parent,
428
- group_depth: group_depth + 1)
445
+ group_depth: group_depth + 1,
446
+ owner_ns: owner_ns)
429
447
  end
430
448
  when Lutaml::Xsd::Sequence, Lutaml::Xsd::Choice, Lutaml::Xsd::All
431
449
  collect_content_particles(particle).any? do |leaf|
432
450
  particle_matches_child?(child, leaf, repository,
433
451
  parent: parent,
434
- group_depth: group_depth)
452
+ group_depth: group_depth,
453
+ owner_ns: owner_ns)
435
454
  end
436
455
  else
437
456
  false
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Xsd
5
- VERSION = "1.2.79"
5
+ VERSION = "1.2.81"
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.79
4
+ version: 1.2.81
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.