moxml 0.5.160 → 0.5.162

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: 3baac5f598cb14a214bcf65d8f19f7d73d6d70c7f3fc0b53766f07bac4f2d176
4
- data.tar.gz: 54e53fb3aa363edd679bca122996d5a224bfa58a99e656a35acc59182dd66f45
3
+ metadata.gz: e3ff9c9a02aad66036e51b20f9dd72fb17adb09b51dbe3950bb5a5174a2f2158
4
+ data.tar.gz: b4924b020e3b4738e08197d918594df3269d5206fda86ab0f582b27e801a1dbf
5
5
  SHA512:
6
- metadata.gz: 705c62afbefc4678ad231e380644023416a5e0cf19e94dd59437c0570abca635e4570db661272490ed1f37804230623d9836e5cc2b8e70619bea1cc16f2fe0a6
7
- data.tar.gz: e40cf23891b36aaca41bb1c62d01b0e7479c5c1c5eada2fa9563f315399d943a170381d5467019e7e956f06940b71aad59db2d19099a545ebf67d7d4603168c8
6
+ metadata.gz: dcb4225b3f662a656232b4e4bccdaefeb261a186d9c01d5369ee54ec28829678202e69349372842e470dcd21bed702f6f28c0e58dc9e45b6c262fffab1ed937a
7
+ data.tar.gz: 22fa41a31b5a8541e2c3f5fa2c8eb088975f173902ff8c046e325ec55c28246e3c459df5e49d004fb2e89ea1d1d8f9adb841d71cae8e4fe8bfaea14cd25c5d7c
data/benchmark/README.md CHANGED
@@ -92,16 +92,16 @@ rewrites this block and `docs/_pages/performance.adoc`):
92
92
  <!-- SCOREBOARD:BEGIN -->
93
93
  | Row | Ratio | Kind | Status |
94
94
  | --- | ----- | ---- | ------ |
95
- | `parse` | 3.59x | parity | met |
96
- | `serialize` | 1.83x | parity | met |
97
- | `xpath //field (count)` | 4.79x | parity | met |
98
- | `xpath //field (each)` | 0.44x | structural | tracked (below parity) |
99
- | `attr pairs listing` | 7.46x | parity | met |
100
- | `build add_element` | 0.57x | structural | tracked (below parity) |
101
- | `walk+read (warm)` | 0.64x | structural | tracked (below parity) |
102
- | `walk+read (cold)` | 0.17x | structural | tracked (below parity), paused: leptris-ruby#527 (1.9.380 crossing regression) |
103
- | `children recursion (cold)` | 0.13x | structural | tracked (below parity), paused: leptris-ruby#527 (1.9.380 crossing regression) |
104
- | `plan records (typed)` | 1.14x | data | informational |
95
+ | `parse` | 4.61x | parity | met |
96
+ | `serialize` | 1.65x | parity | met |
97
+ | `xpath //field (count)` | 3.39x | parity | met |
98
+ | `xpath //field (each)` | 0.34x | structural | tracked (below parity) |
99
+ | `attr pairs listing` | 8.48x | parity | met |
100
+ | `build add_element` | 0.74x | structural | tracked (below parity) |
101
+ | `walk+read (warm)` | 0.54x | structural | tracked (below parity) |
102
+ | `walk+read (cold)` | 0.37x | structural | tracked (below parity), paused: leptris-ruby#527 (1.9.380 crossing regression) |
103
+ | `children recursion (cold)` | 0.45x | structural | tracked (below parity), paused: leptris-ruby#527 (1.9.380 crossing regression) |
104
+ | `plan records (typed)` | 1.13x | data | informational |
105
105
  <!-- SCOREBOARD:END -->
106
106
 
107
107
  The open rows are the known structural gap: every node the facade
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "protocol": {
3
- "measured": "2026-10-23T15:54:45Z",
3
+ "measured": "2026-10-24T05:26:53Z",
4
4
  "ruby": "3.3.13",
5
5
  "adapter": "leptris",
6
6
  "adapter_version": "1.9.381.0",
@@ -8,16 +8,16 @@
8
8
  "reps": 9,
9
9
  "warmup": 2,
10
10
  "clock": "CLOCK_PROCESS_CPUTIME_ID",
11
- "load": 1.87
11
+ "load": 1.74
12
12
  },
13
13
  "rows": [
14
14
  {
15
15
  "row": "parse",
16
16
  "kind": "parity",
17
17
  "parity": true,
18
- "baseline_us": 1027.8,
19
- "candidate_us": 286.6,
20
- "ratio": 3.586,
18
+ "baseline_us": 1075.7,
19
+ "candidate_us": 233.6,
20
+ "ratio": 4.606,
21
21
  "baseline_allocs": 7,
22
22
  "candidate_allocs": 18
23
23
  },
@@ -25,9 +25,9 @@
25
25
  "row": "serialize",
26
26
  "kind": "parity",
27
27
  "parity": true,
28
- "baseline_us": 517.0,
29
- "candidate_us": 281.9,
30
- "ratio": 1.834,
28
+ "baseline_us": 457.0,
29
+ "candidate_us": 277.6,
30
+ "ratio": 1.646,
31
31
  "baseline_allocs": 30,
32
32
  "candidate_allocs": 14
33
33
  },
@@ -35,9 +35,9 @@
35
35
  "row": "xpath //field (count)",
36
36
  "kind": "parity",
37
37
  "parity": true,
38
- "baseline_us": 121.9,
39
- "candidate_us": 25.4,
40
- "ratio": 4.791,
38
+ "baseline_us": 64.4,
39
+ "candidate_us": 19.0,
40
+ "ratio": 3.387,
41
41
  "baseline_allocs": 13,
42
42
  "candidate_allocs": 8
43
43
  },
@@ -45,9 +45,9 @@
45
45
  "row": "xpath //field (each)",
46
46
  "kind": "structural",
47
47
  "parity": false,
48
- "baseline_us": 361.9,
49
- "candidate_us": 822.7,
50
- "ratio": 0.44,
48
+ "baseline_us": 256.4,
49
+ "candidate_us": 744.4,
50
+ "ratio": 0.344,
51
51
  "baseline_allocs": 1213,
52
52
  "candidate_allocs": 10
53
53
  },
@@ -55,9 +55,9 @@
55
55
  "row": "attr pairs listing",
56
56
  "kind": "parity",
57
57
  "parity": true,
58
- "baseline_us": 502.0,
59
- "candidate_us": 67.3,
60
- "ratio": 7.458,
58
+ "baseline_us": 503.6,
59
+ "candidate_us": 59.4,
60
+ "ratio": 8.484,
61
61
  "baseline_allocs": 3300,
62
62
  "candidate_allocs": 0
63
63
  },
@@ -65,19 +65,19 @@
65
65
  "row": "build add_element",
66
66
  "kind": "structural",
67
67
  "parity": false,
68
- "baseline_us": 5532.8,
69
- "candidate_us": 9658.2,
70
- "ratio": 0.573,
68
+ "baseline_us": 5607.1,
69
+ "candidate_us": 7620.4,
70
+ "ratio": 0.736,
71
71
  "baseline_allocs": 20358,
72
- "candidate_allocs": 11363
72
+ "candidate_allocs": 10163
73
73
  },
74
74
  {
75
75
  "row": "walk+read (warm)",
76
76
  "kind": "structural",
77
77
  "parity": false,
78
- "baseline_us": 1805.4,
79
- "candidate_us": 2797.9,
80
- "ratio": 0.645,
78
+ "baseline_us": 1631.1,
79
+ "candidate_us": 3005.5,
80
+ "ratio": 0.543,
81
81
  "baseline_allocs": 4503,
82
82
  "candidate_allocs": 2
83
83
  },
@@ -85,9 +85,9 @@
85
85
  "row": "walk+read (cold)",
86
86
  "kind": "structural",
87
87
  "parity": false,
88
- "baseline_us": 2938.6,
89
- "candidate_us": 17354.2,
90
- "ratio": 0.169,
88
+ "baseline_us": 3094.4,
89
+ "candidate_us": 8308.1,
90
+ "ratio": 0.372,
91
91
  "baseline_allocs": 6011,
92
92
  "candidate_allocs": 10232
93
93
  },
@@ -95,9 +95,9 @@
95
95
  "row": "children recursion (cold)",
96
96
  "kind": "structural",
97
97
  "parity": false,
98
- "baseline_us": 3346.5,
99
- "candidate_us": 25949.4,
100
- "ratio": 0.129,
98
+ "baseline_us": 3414.5,
99
+ "candidate_us": 7574.2,
100
+ "ratio": 0.451,
101
101
  "baseline_allocs": 5711,
102
102
  "candidate_allocs": 8732
103
103
  },
@@ -105,9 +105,9 @@
105
105
  "row": "plan records (typed)",
106
106
  "kind": "data",
107
107
  "parity": false,
108
- "baseline_us": 2901.1,
109
- "candidate_us": 2554.9,
110
- "ratio": 1.136,
108
+ "baseline_us": 3056.3,
109
+ "candidate_us": 2700.7,
110
+ "ratio": 1.132,
111
111
  "baseline_allocs": 8112,
112
112
  "candidate_allocs": 13232
113
113
  }
@@ -41,6 +41,7 @@ Generated by `bundle exec rake docs:coverage` from the coverage matrix and the d
41
41
  | materialization | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
42
42
  | validation | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
43
43
  | validation under-detections | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
44
+ | validation version reporting | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
44
45
  | loud capability contracts | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
45
46
  |===
46
47
 
@@ -14,21 +14,21 @@ the marked block below from a live measurement; see
14
14
  // SCOREBOARD:BEGIN
15
15
  Generated by `SCORE_PUBLISH=1 bundle exec ruby benchmark/scoreboard.rb`. Protocol: CPU-time medians of an interleaved A/B run (raw nokogiri vs moxml on leptris); the ratio is baseline / candidate, so > 1.0 means the facade is faster. Warmups discarded, GC between samples.
16
16
 
17
- Measurement: Ruby 3.3.13, leptris 1.9.381.0, nokogiri 1.19.4, 2026-10-23T15:54:45Z, 1-minute load 1.87, 9 repetitions.
17
+ Measurement: Ruby 3.3.13, leptris 1.9.381.0, nokogiri 1.19.4, 2026-10-24T05:26:53Z, 1-minute load 1.74, 9 repetitions.
18
18
 
19
19
  [cols="2,1,1,1",options="header"]
20
20
  |===
21
21
  | Row | Ratio | Kind | Status
22
- | `parse` | 3.59x | parity | met
23
- | `serialize` | 1.83x | parity | met
24
- | `xpath //field (count)` | 4.79x | parity | met
25
- | `xpath //field (each)` | 0.44x | structural | tracked (below parity)
26
- | `attr pairs listing` | 7.46x | parity | met
27
- | `build add_element` | 0.57x | structural | tracked (below parity)
28
- | `walk+read (warm)` | 0.64x | structural | tracked (below parity)
29
- | `walk+read (cold)` | 0.17x | structural | tracked (below parity), paused: leptris-ruby#527 (1.9.380 crossing regression)
30
- | `children recursion (cold)` | 0.13x | structural | tracked (below parity), paused: leptris-ruby#527 (1.9.380 crossing regression)
31
- | `plan records (typed)` | 1.14x | data | informational
22
+ | `parse` | 4.61x | parity | met
23
+ | `serialize` | 1.65x | parity | met
24
+ | `xpath //field (count)` | 3.39x | parity | met
25
+ | `xpath //field (each)` | 0.34x | structural | tracked (below parity)
26
+ | `attr pairs listing` | 8.48x | parity | met
27
+ | `build add_element` | 0.74x | structural | tracked (below parity)
28
+ | `walk+read (warm)` | 0.54x | structural | tracked (below parity)
29
+ | `walk+read (cold)` | 0.37x | structural | tracked (below parity), paused: leptris-ruby#527 (1.9.380 crossing regression)
30
+ | `children recursion (cold)` | 0.45x | structural | tracked (below parity), paused: leptris-ruby#527 (1.9.380 crossing regression)
31
+ | `plan records (typed)` | 1.13x | data | informational
32
32
  |===
33
33
 
34
34
  *Parity rows* must stay at or above the 0.95 measurement floor. *Structural rows* are measured and trend-locked (10% regression tolerance) while their gaps remain open: per-node wrapper minting costs (#312 endgame) and engine construction. *Data API* rows are informational.
@@ -255,7 +255,7 @@ semantics.
255
255
 
256
256
  === Validation
257
257
 
258
- Pins:: `spec/moxml/xsd_spec.rb`, `spec/moxml/xsd_underdetection_spec.rb`
258
+ Pins:: `spec/moxml/xsd_spec.rb`, `spec/moxml/xsd_underdetection_spec.rb`, `spec/moxml/xsd_version_spec.rb`
259
259
 
260
260
  XSD validation through engines that ship a validator (libxml2;
261
261
  leptris tier-1: recognized roots, one-level content descent, lexical
@@ -265,7 +265,11 @@ where it does not; `[]` means valid. Documents outside the engine's
265
265
  scope validate laxly, except that strict `xs:any` /
266
266
  `xs:anyAttribute` wildcard matches and `xsi:nil` are re-checked from
267
267
  the schema source on every adapter — the re-check can only
268
- under-report, never false-report. Unsupported adapters raise
268
+ under-report, never false-report. A compiled schema reports the
269
+ version its source declares: root `vc:minVersion="1.1"` scopes the
270
+ document to 1.1, anything else is 1.0 (consumers scope 1.0-only
271
+ checks with it; applying 1.1 semantics is the engine-side program).
272
+ Unsupported adapters raise
269
273
  `Moxml::NotImplementedError` at compile; `Moxml::XSD.supported?`
270
274
  answers without raising. RELAX NG rides engines with an RNG face
271
275
  (the same shapes).
@@ -83,6 +83,15 @@ module Moxml
83
83
  ::Leptris::XML::Native.respond_to?(:xpath_result_rows_bulk) &&
84
84
  ::Leptris::XML::Document.method_defined?(:xpath_result_rows_bulk)
85
85
 
86
+ # TODO.sota/04 (build row): ONE-crossing construction —
87
+ # create + attrs + attach (+ optional text) + version bump +
88
+ # consumer mint, all in C off the parent wrapper's struct
89
+ # (leptris-ruby perf/add-element-full). Probed on the class:
90
+ # older exts take the existing two-crossing path.
91
+ ADD_ELEMENT_FULL =
92
+ defined?(::Leptris::XML::NativeNode) &&
93
+ ::Leptris::XML::NativeNode.method_defined?(:add_element_full)
94
+
86
95
  # leptris-ruby#460: set-text by document+address, kind-dispatched
87
96
  # in C — no binding twin mint per write. Face-probed, not
88
97
  # version-gated (the floor admits bindings without it).
@@ -1334,8 +1343,6 @@ module Moxml
1334
1343
  end
1335
1344
 
1336
1345
  def set_root(doc, element)
1337
- drop_columnar_children(doc)
1338
- drop_columnar_children(element)
1339
1346
  to_binding(doc).root = to_binding(element)
1340
1347
  end
1341
1348
 
@@ -1735,7 +1742,6 @@ module Moxml
1735
1742
  # re-indented). Raw-pointer walk over the root subtree — the
1736
1743
  # same batch child-pointer pattern the materializer uses.
1737
1744
  def strip_blank_text_nodes!(doc)
1738
- drop_columnar_children(doc)
1739
1745
  binding_ffi = ::Leptris::XML::FFI
1740
1746
  root = binding_ffi.leptris_document_root(doc.c_ptr)
1741
1747
  return unless root && !root.null?
@@ -2233,8 +2239,6 @@ module Moxml
2233
2239
  # source. Answer the installed binding element; callers re-point
2234
2240
  # wrappers at it.
2235
2241
  def adopt_root(doc, element)
2236
- drop_columnar_children(doc)
2237
- drop_columnar_children(element)
2238
2242
  return nil unless NATIVE_ROOT_ADOPT
2239
2243
 
2240
2244
  binding_doc = to_binding(doc)
@@ -2345,7 +2349,10 @@ module Moxml
2345
2349
  doc = doc_for(node)
2346
2350
  return nil unless doc
2347
2351
 
2348
- map = attachments.get(doc, COLUMNAR_CHILDREN)
2352
+ pair = attachments.get(doc, COLUMNAR_CHILDREN)
2353
+ return nil unless pair.is_a?(Array) && pair[0] == doc.version
2354
+
2355
+ map = pair[1]
2349
2356
  return nil unless map&.key?(node)
2350
2357
 
2351
2358
  map[node].dup
@@ -2383,36 +2390,14 @@ module Moxml
2383
2390
  end
2384
2391
  mine << handle if depth == child_depth
2385
2392
  end
2386
- attachments.set(doc, COLUMNAR_CHILDREN, map) if map
2393
+ # Memo-version law: the pair is stamped with the binding
2394
+ # Document#@version the map was primed under; every mutation
2395
+ # face advances that version in C, so a stale map misses the
2396
+ # hit check and re-primes — no write-side drop walk needed.
2397
+ attachments.set(doc, COLUMNAR_CHILDREN, [doc.version, map]) if map
2387
2398
  mine
2388
2399
  end
2389
2400
 
2390
- # Structural seams drop the columnar map wholesale; the next
2391
- # children read re-primes it (see COLUMNAR_WALK). Cheap no-op
2392
- # when no map is live: one lock-free attachment probe.
2393
- def drop_columnar_children(node)
2394
- return unless COLUMNAR_WALK
2395
-
2396
- doc =
2397
- case node
2398
- when ::Leptris::XML::NativeNode
2399
- doc_for(node)
2400
- when ::Leptris::XML::Document
2401
- node
2402
- else
2403
- begin
2404
- node.document
2405
- rescue StandardError
2406
- nil
2407
- end
2408
- end
2409
- return if doc.nil?
2410
-
2411
- return if attachments.get(doc, COLUMNAR_CHILDREN).nil?
2412
-
2413
- attachments.delete(doc, COLUMNAR_CHILDREN)
2414
- end
2415
-
2416
2401
  def native_keep_entity_refs?(node)
2417
2402
  return false unless NATIVE_READ_LAYER &&
2418
2403
  node.is_a?(::Leptris::XML::NativeNode)
@@ -2625,8 +2610,18 @@ module Moxml
2625
2610
  # loop is C->C dlsym). Requires parent binding + the face;
2626
2611
  # callers fall back to create_element + set_attribute +
2627
2612
  # add_child when absent.
2613
+ # TODO.sota/04: the ONE-crossing build face. Returns the
2614
+ # contract-carrying row, or nil when this binding cannot
2615
+ # serve it (caller takes the legacy path).
2616
+ def add_element_full(parent, name, attrs)
2617
+ return nil unless NATIVE_C_MINT && ADD_ELEMENT_FULL &&
2618
+ parent.is_a?(::Leptris::XML::NativeNode)
2619
+
2620
+ validate_element_name(name)
2621
+ parent.add_element_full(name.to_s, attrs.flatten, nil)
2622
+ end
2623
+
2628
2624
  def create_element_with_attrs(parent, name, attrs)
2629
- drop_columnar_children(parent)
2630
2625
  validate_element_name(name)
2631
2626
  # NativeNode parents need NO FFI twin (TODO.sota/04 bridge
2632
2627
  # tax): the bridge existed only to obtain c_address +
@@ -2964,8 +2959,6 @@ module Moxml
2964
2959
  # on the source, so a living source can never outlive the
2965
2960
  # memory its reads still touch.
2966
2961
  def absorb_source_document(dest, owner)
2967
- drop_columnar_children(dest)
2968
- drop_columnar_children(owner)
2969
2962
  return nil unless NATIVE_DOC_ABSORB
2970
2963
  return :direct if attachments.get(dest, :absorbed_sources)&.include?(owner)
2971
2964
 
@@ -3011,8 +3004,6 @@ module Moxml
3011
3004
  end
3012
3005
 
3013
3006
  def add_child(parent, child)
3014
- drop_columnar_children(parent)
3015
- drop_columnar_children(child)
3016
3007
  result = add_child_impl(parent, child)
3017
3008
  # A successful child insertion rebuilds the engine's
3018
3009
  # document index; the upstream sibling-insert bug leaves it
@@ -3134,8 +3125,6 @@ module Moxml
3134
3125
  end
3135
3126
 
3136
3127
  def add_previous_sibling(node, new_node)
3137
- drop_columnar_children(node)
3138
- drop_columnar_children(new_node)
3139
3128
  result = add_previous_sibling_impl(node, new_node)
3140
3129
  mark_xpath_index_stale(node)
3141
3130
  result
@@ -3188,8 +3177,6 @@ module Moxml
3188
3177
  end
3189
3178
 
3190
3179
  def add_next_sibling(node, new_node)
3191
- drop_columnar_children(node)
3192
- drop_columnar_children(new_node)
3193
3180
  result = add_next_sibling_impl(node, new_node)
3194
3181
  mark_xpath_index_stale(node)
3195
3182
  result
@@ -3266,7 +3253,6 @@ module Moxml
3266
3253
  end
3267
3254
 
3268
3255
  def remove(node)
3269
- drop_columnar_children(node)
3270
3256
  node = to_binding(node) if NATIVE_READ_LAYER
3271
3257
  case node
3272
3258
  when CustomizedLeptris::Declaration
@@ -3298,8 +3284,6 @@ module Moxml
3298
3284
  end
3299
3285
 
3300
3286
  def replace(node, new_node)
3301
- drop_columnar_children(node)
3302
- drop_columnar_children(new_node)
3303
3287
  if NATIVE_READ_LAYER
3304
3288
  node = to_binding(node)
3305
3289
  new_node = to_binding(new_node)
@@ -3358,7 +3342,6 @@ module Moxml
3358
3342
  Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.290.0")
3359
3343
 
3360
3344
  def replace_children(node, new_children)
3361
- drop_columnar_children(node)
3362
3345
  # Cross-pool children: the binding's bulk face adopts per
3363
3346
  # node and fails on mixed kinds (inner_xml= with
3364
3347
  # text+elements died with Memory allocation failed —
@@ -3448,7 +3431,6 @@ module Moxml
3448
3431
  ::Leptris::XML::NativeNode.method_defined?(:set_text_native)
3449
3432
 
3450
3433
  def set_text_content(node, content)
3451
- drop_columnar_children(node)
3452
3434
  if SET_TEXT_NATIVE && node.is_a?(::Leptris::XML::NativeNode)
3453
3435
  begin
3454
3436
  node.set_text_native(content.to_s)
data/lib/moxml/element.rb CHANGED
@@ -139,10 +139,17 @@ module Moxml
139
139
  # wrapper. Equivalent to create_element + attribute writes +
140
140
  # add_child, minus two crossings and the intermediate churn.
141
141
  def add_element(name, attrs = {})
142
+ # TODO.sota/04: one crossing when the binding serves it.
143
+ native_child = adapter.add_element_full(@native, name, attrs)
144
+ if native_child
145
+ attach_built_child(native_child)
146
+ return Moxml::Node.wrap_with(native_child, context, adapter)
147
+ end
142
148
  native_child = adapter.create_element_with_attrs(
143
149
  @native, name, attrs
144
150
  )
145
151
  if native_child
152
+ attach_built_child(native_child)
146
153
  Moxml::Node.wrap_with(native_child, context, adapter)
147
154
  else
148
155
  fallback = Moxml::Node.wrap_with(
@@ -154,6 +161,16 @@ module Moxml
154
161
  end
155
162
  end
156
163
 
164
+ # The construction fast paths bypass add_child, so they owe the
165
+ # same invalidation contract: the ctx children clock retires every
166
+ # wrapper's generation-checked memo (the receiver's own included —
167
+ # the read re-checks the generation before serving).
168
+ def attach_built_child(native_child)
169
+ context.bump_children_generation
170
+ native_child.parent_node = self
171
+ native_child
172
+ end
173
+
157
174
  def attributes
158
175
  # Primed like Node.wrap: the adapter and type are known at
159
176
  # mint, so the first name/value/attribute? access skips the
data/lib/moxml/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Moxml
4
- VERSION = "0.5.160"
4
+ VERSION = "0.5.162"
5
5
  end
@@ -67,7 +67,11 @@ module Moxml
67
67
  :declared_attributes, :any_attribute)
68
68
  SchemaIndex = Struct.new(:target_namespace, :element_default,
69
69
  :attribute_default, :elements,
70
- :attributes, :contents)
70
+ :attributes, :contents, :version)
71
+
72
+ # The XSD 1.1 version-tagging namespace (Structures §C.3):
73
+ # vc:minVersion / vc:maxVersion gate 1.1 constructs.
74
+ VC_NS = "http://www.w3.org/2001/XMLSchema-versioning"
71
75
 
72
76
  class << self
73
77
  # Index one schema document. Returns nil when the source
@@ -81,7 +85,7 @@ module Moxml
81
85
  root["targetNamespace"].to_s,
82
86
  root["elementFormDefault"] == "qualified",
83
87
  root["attributeFormDefault"] == "qualified",
84
- {}, {}, {}
88
+ {}, {}, {}, schema_version(root)
85
89
  )
86
90
  child_elements(root).each do |decl|
87
91
  case decl.name
@@ -94,6 +98,20 @@ module Moxml
94
98
  nil
95
99
  end
96
100
 
101
+ # The effective schema version declared at the root
102
+ # (#460 item 2): vc:minVersion="1.1" on <xs:schema> scopes
103
+ # the document to 1.1 semantics; anything else reports
104
+ # 1.0. Conditional (subtree-level) vc: evaluation is the
105
+ # engine's conditional-assignment work — the facade reports
106
+ # the root declaration only.
107
+ def schema_version(root)
108
+ declared = root.attributes.find do |attribute|
109
+ attribute.namespace&.uri == VC_NS &&
110
+ attribute.name == "minVersion"
111
+ end&.value
112
+ declared.to_s.start_with?("1.1") ? "1.1" : "1.0"
113
+ end
114
+
97
115
  # Post-validation findings for one instance document.
98
116
  def findings(doc, index)
99
117
  return [] unless index
data/lib/moxml/xsd.rb CHANGED
@@ -175,6 +175,15 @@ module Moxml
175
175
  @context.config.adapter.xsd_declarations(@handle)
176
176
  end
177
177
 
178
+ # The effective schema version the source declares (#460 item
179
+ # 2): "1.1" when the root carries vc:minVersion="1.1", else
180
+ # "1.0". Consumers (lutaml-xsd) scope 1.0-only checks with
181
+ # it; the engine's application of 1.1 semantics is the
182
+ # engine-side program.
183
+ def version
184
+ @rules_index ? @rules_index.version : "1.0"
185
+ end
186
+
178
187
  # Deterministic drop (#362.3): true when the adapter freed
179
188
  # the engine-side schema NOW; false when the adapter can only
180
189
  # wait for GC (documented per adapter). After a true release
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ # The COLUMNAR_WALK children map must never serve rows older than a
6
+ # completed mutation, through EVERY write lane. This is the memo-version
7
+ # law (leptris-ruby#530) applied to the columnar map: the binding
8
+ # Document#@version advances on every mutation, so a map primed under
9
+ # an older version is stale by construction — independent of which
10
+ # adapter seam (or none) happens to drop it.
11
+ RSpec.describe "columnar children map invalidation" do
12
+ let(:ctx) { Moxml.new(:leptris) }
13
+
14
+ def walk_children(node)
15
+ node.children.map { |c| c.is_a?(Moxml::Element) ? c.name : c.text }
16
+ end
17
+
18
+ it "serves fresh rows after content= replaces a child subtree" do
19
+ doc = ctx.parse("<r><a>t1</a><b>t2</b></r>")
20
+ a = doc.root.children.first
21
+ walk_children(a) # prime the columnar map over a's subtree
22
+ a.content = "replaced"
23
+ expect(walk_children(a)).to eq(["replaced"])
24
+ expect(walk_children(doc.root)).to eq(%w[a b])
25
+ end
26
+
27
+ it "serves fresh rows after add_element" do
28
+ doc = ctx.parse("<r><a/></r>")
29
+ walk_children(doc.root)
30
+ doc.root.add_element("b", "k" => "v")
31
+ expect(walk_children(doc.root)).to eq(%w[a b])
32
+ end
33
+
34
+ it "serves fresh rows after add_next_sibling" do
35
+ doc = ctx.parse("<r><a/></r>")
36
+ a = doc.root.children.first
37
+ walk_children(doc.root)
38
+ a.add_next_sibling("<c/>")
39
+ expect(walk_children(doc.root)).to eq(%w[a c])
40
+ end
41
+
42
+ it "serves fresh rows after remove" do
43
+ doc = ctx.parse("<r><a/><b/></r>")
44
+ b = doc.root.children.last
45
+ walk_children(doc.root)
46
+ b.remove
47
+ expect(walk_children(doc.root)).to eq(%w[a])
48
+ end
49
+
50
+ it "keeps the primed rows valid when nothing mutated" do
51
+ doc = ctx.parse("<r><a>t1</a><b>t2</b></r>")
52
+ expected = walk_children(doc.root)
53
+ expect(walk_children(doc.root)).to eq(expected)
54
+ expect(walk_children(doc.root)).to eq(expected)
55
+ end
56
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ # Processing model §Validation (normative): pinned here.
6
+
7
+ # #460 item 2: a compiled schema reports the effective version its
8
+ # source declares — vc:minVersion="1.1" on the root scopes the
9
+ # document to 1.1 semantics; anything else reports 1.0. The
10
+ # detection is namespace-based (vc: is a convention, not a
11
+ # requirement) and reads the ROOT declaration; conditional
12
+ # (subtree-level) vc: evaluation is the engine's program.
13
+ RSpec.describe "Moxml::XSD::Schema#version (#460 item 2)" do
14
+ %i[nokogiri leptris].each do |adapter_name|
15
+ describe "on #{adapter_name}" do
16
+ let(:ctx) { Moxml.new(adapter_name) }
17
+
18
+ before do
19
+ skip "#{adapter_name} requires an XSD-capable engine" unless
20
+ Moxml::XSD.supported?(ctx)
21
+ end
22
+
23
+ it "reports 1.1 when the root declares vc:minVersion=1.1" do
24
+ schema = ctx.xsd(<<~XSD)
25
+ <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
26
+ xmlns:vc="http://www.w3.org/2001/XMLSchema-versioning"
27
+ vc:minVersion="1.1">
28
+ <xs:element name="note" type="xs:string"/>
29
+ </xs:schema>
30
+ XSD
31
+ expect(schema.version).to eq("1.1")
32
+ end
33
+
34
+ it "detects the versioning namespace, not the prefix" do
35
+ schema = ctx.xsd(<<~XSD)
36
+ <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
37
+ xmlns:vcx="http://www.w3.org/2001/XMLSchema-versioning"
38
+ vcx:minVersion="1.1">
39
+ <xs:element name="note" type="xs:string"/>
40
+ </xs:schema>
41
+ XSD
42
+ expect(schema.version).to eq("1.1")
43
+ end
44
+
45
+ it "reports 1.0 without a root version declaration" do
46
+ schema = ctx.xsd(<<~XSD)
47
+ <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
48
+ <xs:element name="note" type="xs:string"/>
49
+ </xs:schema>
50
+ XSD
51
+ expect(schema.version).to eq("1.0")
52
+ end
53
+
54
+ it "reports 1.0 for an explicit root vc:minVersion=1.0" do
55
+ schema = ctx.xsd(<<~XSD)
56
+ <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
57
+ xmlns:vc="http://www.w3.org/2001/XMLSchema-versioning"
58
+ vc:minVersion="1.0">
59
+ <xs:element name="note" type="xs:string"/>
60
+ </xs:schema>
61
+ XSD
62
+ expect(schema.version).to eq("1.0")
63
+ end
64
+
65
+ it "reports 1.0 when only a construct carries vc:minVersion" do
66
+ schema = ctx.xsd(<<~XSD)
67
+ <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
68
+ xmlns:vc="http://www.w3.org/2001/XMLSchema-versioning">
69
+ <xs:element name="note">
70
+ <xs:complexType>
71
+ <xs:sequence vc:minVersion="1.1">
72
+ <xs:element name="a"/>
73
+ </xs:sequence>
74
+ </xs:complexType>
75
+ </xs:element>
76
+ </xs:schema>
77
+ XSD
78
+ expect(schema.version).to eq("1.0")
79
+ end
80
+ end
81
+ end
82
+ end
@@ -85,6 +85,10 @@ module ModelCoverage
85
85
  section: "Validation",
86
86
  pin: "spec/moxml/xsd_underdetection_spec.rb",
87
87
  },
88
+ "validation version reporting" => {
89
+ section: "Validation",
90
+ pin: "spec/moxml/xsd_version_spec.rb",
91
+ },
88
92
  "loud capability contracts" => {
89
93
  section: "Errors and loud degradation",
90
94
  pin: "spec/consistency/loud_degradation_spec.rb",
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: moxml
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.160
4
+ version: 0.5.162
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -477,6 +477,7 @@ files:
477
477
  - spec/moxml/adapter/.gitkeep
478
478
  - spec/moxml/adapter/README.md
479
479
  - spec/moxml/adapter/base_spec.rb
480
+ - spec/moxml/adapter/columnar_invalidation_spec.rb
480
481
  - spec/moxml/adapter/entity_restoration_spec.rb
481
482
  - spec/moxml/adapter/headed_ox_spec.rb
482
483
  - spec/moxml/adapter/leptris_adoption_spec.rb
@@ -633,6 +634,7 @@ files:
633
634
  - spec/moxml/xsd_release_spec.rb
634
635
  - spec/moxml/xsd_spec.rb
635
636
  - spec/moxml/xsd_underdetection_spec.rb
637
+ - spec/moxml/xsd_version_spec.rb
636
638
  - spec/moxml/xslt/stylesheet_spec.rb
637
639
  - spec/performance/README.md
638
640
  - spec/performance/benchmark_spec.rb