lutaml-xsd 1.2.80 → 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: 0aed69846e615ba0095fd9030ca3c7d870fe590c855694797393e669a57085ec
4
- data.tar.gz: abcb15e8a7482e4589d08bbb886529f6aff6462db5c56e4626c9610dcb25777f
3
+ metadata.gz: 467b4c2343707af1d200289a1666d12e3ab984c438f0a29ef601d92143914562
4
+ data.tar.gz: d249de5b32c7fb201c8025cf56af088bde07acb6e1f76d2cd3ae1f15bd39cf29
5
5
  SHA512:
6
- metadata.gz: 928180700fa58c7965b7f57729b681a1580f18c5da136c6b7729a013426ca5bfeaad44b686ce0e28c2eb1247dd3e73ca1a1e34c81b61c37030165bf4f297ae62
7
- data.tar.gz: 50441ce2cda522fed7e3df0acd8843ef3824a72b685bf663e21ade1d9c5a2366fbb2af244b4f83d839dbdb4e2c41faf3cf1eb14f3376bef8c0ef15f03de9fb98
6
+ metadata.gz: 0f9c9c9c71bf3e064dd93ab6e007df3f9d90aaad2478bf77d8f7b7915804b5e8218a6d8adb97be02293560dc261ed957a6fe09af5404a30b18ee7a0d5d1a941e
7
+ data.tar.gz: 539a1503358510cde20356dc3d6a0a1a2c0b9daaebf77bda9ef45bc54f45222a0eaf2d97477b05f1347b1d35dcc22742963b0fc388823217bc6cd45ceb900bdf
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 | 4563/4696 (97.17%) |
123
+ | pipeline | 4573/4696 (97.38%) |
124
124
 
125
125
  (2026-11-02, bundled-stack measurement with the escape,
126
126
  hint-following, pattern-alternative, identity-crash, engine-set,
@@ -142,7 +142,7 @@ lockfile refresh) → 4411 (93.9%) → 4430 (94.3%) → 4435 (94.4%) →
142
142
  4436 (94.5%) → 4439 (94.5%) → 4454 (94.9%) → 4495 (95.7%) → 4524
143
143
  (96.3%) → 4534 (96.6%) → 4540 (96.7%) → 4542 (96.7%) → 4546 (96.8%)
144
144
  → 4557 (97.0%) → 4565 (97.2%) → 4569 (97.3%) → 4562 (drift raw) →
145
- 4563 (97.17%, ref-namespace round), +602 net over the 1.2.43 baseline. The escapes round (PR #197 — \d→\p{Nd},
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},
146
146
  \w/\W/\s to their XSD value spaces) collapses Regex 120→69;
147
147
  same-step pattern facets now behave as alternatives and
148
148
  whitespace-only text reaches the facets intact (PR #200), Regex
@@ -422,6 +422,34 @@ tuple keys, node-set field violations and absent-field rows handled, cross-eleme
422
422
  4569→4562 raw; 4563 with this round. NET -6 documented, not
423
423
  chased — the fixed-empty leg reverses the moment leptris-rs#25
424
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).
425
453
  - REGEX SEMANTICS ROUND (2026-10-31, PR #208): \w/\W now carry their
426
454
  Appendix F complements (\w = [^\p{P}\p{Z}\p{C}], class interiors
427
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
@@ -299,6 +299,7 @@ module Lutaml
299
299
  type.complex_content
300
300
  content = type.complex_content
301
301
  extension = Array(content.extension).first
302
+ restriction = Array(content.restriction).first
302
303
  if extension
303
304
  base_local = extension.base.to_s.split(":").last
304
305
  base_type = base_local && repository&.resolve_named_type(base_local)
@@ -311,6 +312,14 @@ module Lutaml
311
312
  particles << particle
312
313
  end
313
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
314
323
  end
315
324
  end
316
325
 
@@ -508,12 +517,16 @@ module Lutaml
508
517
 
509
518
  # A qualified ref names a declaration in ONE namespace.
510
519
  # Global names duplicated across namespaces (import + main)
511
- # resolve against the child's namespace today; when the owner
512
- # schema's target namespace declares the same local name, a
513
- # foreign child is an unexpected element (the engine — fed
514
- # the documents one at a time — admits the substitute, so
515
- # this runs on the engine-accept path). Wildcard territory
516
- # is exempt.
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.
517
530
  #
518
531
  # @return [void]
519
532
  def scan_cross_namespace_refs(xml_element, particles, collector,
@@ -526,12 +539,35 @@ module Lutaml
526
539
  end
527
540
  return if refs.empty?
528
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
+
529
552
  xml_element.children.each do |child|
530
- next if child.namespace_uri.to_s == owner_ns
531
- next unless refs.any? do |r|
532
- r.ref.to_s.split(":").last == child.name
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
533
569
  end
534
- next unless repository.resolve(child.name, owner_ns)
570
+ next unless hit
535
571
  next if wildcard_covers_child?(xml_element, particles, child,
536
572
  repository)
537
573
 
@@ -540,7 +576,7 @@ module Lutaml
540
576
  code: "unexpected_element_in_sequence",
541
577
  message: "Unexpected element '#{child.qualified_name}' " \
542
578
  "in sequence — the reference names the same " \
543
- "declaration in #{owner_ns}",
579
+ "declaration in another namespace",
544
580
  location: child.xpath,
545
581
  context: { element: child.qualified_name },
546
582
  suggestion: "Use the declaration's namespace or rename " \
@@ -563,7 +599,7 @@ module Lutaml
563
599
  return nil unless repository
564
600
 
565
601
  local = particle.ref.to_s.split(":").last
566
- (owner_ns && repository.resolve(local, owner_ns)) ||
602
+ (owner_ns && repository.resolve_global(local, owner_ns)) ||
567
603
  repository.resolve(local, child.namespace_uri) ||
568
604
  repository.resolve(local, nil)
569
605
  end
@@ -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
@@ -252,12 +252,17 @@ module Lutaml
252
252
  # owner_ns nil means UNKNOWN (the caller has no owner
253
253
  # context), not "no namespace" — resolve(local, nil) only
254
254
  # matches no-namespace declarations, and a hint-loaded
255
- # document can supply a foreign namesake.
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.
256
258
  declaration =
257
- (owner_ns && repository&.resolve(local, owner_ns)) ||
259
+ (owner_ns && repository&.resolve_global(local, owner_ns)) ||
258
260
  repository&.resolve(local, child.namespace_uri) ||
259
261
  repository&.resolve(local, nil)
260
- 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
261
266
 
262
267
  child.name == declaration.name &&
263
268
  namespaces_match?(child.namespace_uri,
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Xsd
5
- VERSION = "1.2.80"
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.80
4
+ version: 1.2.81
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.