moxml 0.5.148 → 0.5.150

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.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/dependent-repos.json +7 -2
  3. data/.github/workflows/dependent-tests.yml +4 -1
  4. data/.github/workflows/nightly-fuzz.yml +80 -0
  5. data/.github/workflows/performance-release.yml +79 -0
  6. data/.github/workflows/performance.yml +59 -0
  7. data/Rakefile +23 -0
  8. data/benchmark/README.md +106 -0
  9. data/benchmark/scoreboard.json +115 -0
  10. data/benchmark/scoreboard.rb +556 -0
  11. data/bin/generate-model-coverage +21 -0
  12. data/docs/_pages/adapter-compatibility.adoc +3 -3
  13. data/docs/_pages/divergences.adoc +88 -39
  14. data/docs/_pages/ecosystem.adoc +62 -0
  15. data/docs/_pages/headed-ox-limitations.adoc +1 -1
  16. data/docs/_pages/headed-ox.adoc +2 -2
  17. data/docs/_pages/index.adoc +4 -0
  18. data/docs/_pages/model-coverage.adoc +57 -0
  19. data/docs/_pages/performance.adoc +29 -0
  20. data/docs/_pages/why-moxml.adoc +87 -0
  21. data/docs/_references/processing-model.adoc +69 -3
  22. data/lib/compat/opal/moxml_boot.rb +4 -0
  23. data/lib/moxml/adapter/base.rb +73 -0
  24. data/lib/moxml/adapter/leptris.rb +293 -23
  25. data/lib/moxml/adapter/libxml.rb +179 -25
  26. data/lib/moxml/adapter/nokogiri.rb +10 -0
  27. data/lib/moxml/adapter/oga.rb +71 -9
  28. data/lib/moxml/adapter/ox.rb +34 -2
  29. data/lib/moxml/adapter/rexml.rb +82 -17
  30. data/lib/moxml/c14n/data_model.rb +8 -0
  31. data/lib/moxml/c14n/processor.rb +17 -8
  32. data/lib/moxml/context.rb +33 -1
  33. data/lib/moxml/document.rb +18 -4
  34. data/lib/moxml/node.rb +75 -8
  35. data/lib/moxml/node_set.rb +42 -1
  36. data/lib/moxml/reference/normalizer.rb +63 -0
  37. data/lib/moxml/reference/parser.rb +417 -0
  38. data/lib/moxml/reference/writer.rb +121 -0
  39. data/lib/moxml/reference.rb +205 -0
  40. data/lib/moxml/relaxng.rb +62 -0
  41. data/lib/moxml/sax/block_handler.rb +7 -0
  42. data/lib/moxml/sax/handler.rb +20 -0
  43. data/lib/moxml/version.rb +1 -1
  44. data/lib/moxml/xpath/compiler.rb +17 -2
  45. data/lib/moxml/xpath/ordering.rb +97 -0
  46. data/lib/moxml/xpath.rb +29 -0
  47. data/lib/moxml.rb +2 -0
  48. data/spec/consistency/adapter_parity_spec.rb +24 -0
  49. data/spec/consistency/citation_hygiene_spec.rb +30 -0
  50. data/spec/consistency/differential_taxonomy_spec.rb +117 -0
  51. data/spec/consistency/divergence_register_spec.rb +9 -1
  52. data/spec/consistency/eol_normalization_spec.rb +2 -0
  53. data/spec/consistency/loud_degradation_spec.rb +2 -0
  54. data/spec/consistency/metamorphic_properties_spec.rb +195 -0
  55. data/spec/consistency/metamorphic_spec.rb +138 -0
  56. data/spec/consistency/model_coverage_audit_spec.rb +144 -61
  57. data/spec/consistency/published_docs_spec.rb +49 -0
  58. data/spec/consistency/reference_differential_spec.rb +186 -0
  59. data/spec/consistency/xpath_idioms_spec.rb +34 -5
  60. data/spec/consistency/xslt_spec.rb +2 -0
  61. data/spec/integration/shared_examples/edge_cases.rb +0 -13
  62. data/spec/moxml/adapter/leptris_attribute_snapshot_spec.rb +10 -7
  63. data/spec/moxml/adapter/leptris_spec.rb +23 -3
  64. data/spec/moxml/adapter/ox_spec.rb +1 -1
  65. data/spec/moxml/adapter/shared_examples/adapter_contract.rb +0 -8
  66. data/spec/moxml/attribute_resolver_spec.rb +35 -0
  67. data/spec/moxml/c14n/adapter_delegation_spec.rb +35 -6
  68. data/spec/moxml/c14n/character_encoding_spec.rb +7 -13
  69. data/spec/moxml/c14n/comments_pis_spec.rb +35 -8
  70. data/spec/moxml/c14n/subset_spec.rb +2 -12
  71. data/spec/moxml/c14n/w3c_examples_spec.rb +2 -5
  72. data/spec/moxml/doctype_spec.rb +7 -0
  73. data/spec/moxml/document_parse_diagnostics_spec.rb +2 -0
  74. data/spec/moxml/element_indent_spec.rb +34 -0
  75. data/spec/moxml/entity_spec.rb +2 -0
  76. data/spec/moxml/node_spec.rb +0 -3
  77. data/spec/moxml/plan_conformance_spec.rb +2 -0
  78. data/spec/moxml/processing_instruction_spec.rb +9 -0
  79. data/spec/moxml/reference_opal_spec.rb +29 -0
  80. data/spec/moxml/reference_spec.rb +162 -0
  81. data/spec/moxml/relax_ng_spec.rb +67 -0
  82. data/spec/moxml/sax_spec.rb +47 -74
  83. data/spec/moxml/text_spec.rb +2 -0
  84. data/spec/moxml/to_html_spec.rb +2 -0
  85. data/spec/moxml/xhtml_output_spec.rb +2 -0
  86. data/spec/moxml/xpath/axes_spec.rb +2 -2
  87. data/spec/moxml/xpath_capabilities_spec.rb +11 -38
  88. data/spec/moxml/xsd_spec.rb +38 -0
  89. data/spec/support/input_generator.rb +154 -0
  90. data/spec/support/input_taxonomy.rb +219 -0
  91. data/spec/support/model_coverage.rb +159 -0
  92. data/spec/support/xml_generator.rb +54 -0
  93. metadata +32 -2
@@ -19,72 +19,121 @@ here. Contract:
19
19
  |===
20
20
  | ID | Adapter(s) | Divergence
21
21
 
22
- | D-01
23
- | oga
24
- | The `xmlns:` default-namespace XPath idiom is unsupported by oga's
25
- own engine (explicit bindings work). Pinned in
26
- `spec/consistency/xpath_idioms_spec.rb`.
27
-
28
22
  | D-02
29
23
  | oga
30
24
  | Raw CR preserved in text content — §2.11 EOL normalization not
31
- applied at parse. Pinned in `spec/consistency/eol_normalization_spec.rb`.
25
+ applied at parse. Decided permanent: normalization belongs at the
26
+ engine's parse layer; a facade read arm would only cover the read
27
+ verbs it touches and change content/source-position consistency.
28
+ Pinned in `spec/consistency/eol_normalization_spec.rb`.
32
29
 
33
30
  | D-03
34
31
  | ox, headed_ox
35
- | `
` character references collapse to LF. Pinned in
32
+ | `
` character references collapse to LF. Decided permanent:
33
+ the reference-vs-literal distinction is lost at parse, so no facade
34
+ read can recover it. Pinned in
36
35
  `spec/consistency/eol_normalization_spec.rb`.
37
36
 
38
- | D-04
39
- | oga
40
- | The XML declaration surfaces as a `<?xml?>` PI; PI content keeps its
41
- separator space. Pinned in `spec/moxml/c14n/adapter_delegation_spec.rb`.
42
-
43
37
  | D-05
44
38
  | ox, headed_ox
45
- | PI content dropped at parse. Pinned in
46
- `spec/moxml/c14n/adapter_delegation_spec.rb`.
39
+ | PI content is dropped on parse for element-child PIs, XPath-minted
40
+ wrappers, and quoted data (some document-level shapes retain it).
41
+ Pinned in `spec/moxml/c14n/adapter_delegation_spec.rb` and the
42
+ taxonomy differential.
47
43
 
48
44
  | D-06
49
45
  | ox, headed_ox
50
46
  | Comment content padding stripped at parse. Pinned in
51
47
  `spec/moxml/c14n/subset_spec.rb`.
52
48
 
53
- | D-07
54
- | rexml
55
- | `text()` node tests return no text nodes (adapter XPath capability
56
- gap, both axes). Pinned in `spec/moxml/c14n/subset_spec.rb`.
57
-
58
- | D-08
59
- | c14n subsystem
60
- | Document-level PI/comment rendering not yet implemented (all
61
- adapters; a subsystem TODO, not an engine divergence). Tracked in
62
- `TODO.c14n/09-comments-pis-tests.md`.
63
-
64
49
  | D-09
65
50
  | oga, ox, headed_ox, libxml, leptris
66
- | Element-level `to_xml(indent:)` silently ignored (libxml emits a
67
- partial, unindented newline form). Document-level HTML/XHTML
68
- indentation is honored on leptris/nokogiri and raises on incapable
69
- engines. Pinned in
51
+ | Element-level `to_xml(indent:)` is not supported by these engines:
52
+ the facade raises the named `Moxml::AdapterError` instead of
53
+ answering unindented output; `indent: 0` and the no-option path stay
54
+ common. The engine-level `serialize(indent:)` lane (direct
55
+ `Adapter.serialize`) still ignores or partially applies the knob.
56
+ Pinned in `spec/moxml/element_indent_spec.rb` and
70
57
  `spec/moxml/adapter/shared_examples/adapter_contract.rb`.
71
58
 
72
59
  | D-10
73
60
  | ox, headed_ox
74
- | SAX emits no separate CDATA/comment/processing-instruction events
75
- (sequences fold into text or are dropped). Pinned in
61
+ | SAX reports no separate CDATA/comment/processing-instruction
62
+ events. A handler that subscribes to one of those families gets the
63
+ named `Moxml::NotImplementedError` — loud, never a silently
64
+ incomplete stream. Text-only handlers still receive CDATA folded
65
+ into character data as the engine delivers it. Pinned in
76
66
  `spec/moxml/sax_spec.rb`.
77
67
 
78
68
  | D-11
79
69
  | oga
80
70
  | SAX parse is more lenient with malformed input than the model's
81
- error authority. Pinned in `spec/moxml/sax_spec.rb`.
71
+ error authority. Decided permanent: the engine owns error reporting,
72
+ and pre-validating with a strict parse would defeat the streaming
73
+ lane. Pinned in `spec/moxml/sax_spec.rb`.
82
74
 
83
- | D-12
75
+ | D-13
84
76
  | ox, headed_ox
85
- | The XPath translation covers a subset: no positional predicates,
86
- attribute-value predicates, namespace/default-namespace tests,
87
- `position()`/`count()`, `text()`, boolean expressions, `child::`/
88
- `descendant::` axes, or unions. Pinned in
89
- `spec/moxml/xpath_capabilities_spec.rb` and `spec/moxml/node_spec.rb`.
77
+ | Inter-element whitespace-only text nodes are dropped at parse
78
+ (node counts and leading/trailing text differ from the other
79
+ adapters). Surfaced by the taxonomy differential. Pinned in
80
+ `spec/consistency/differential_taxonomy_spec.rb`.
81
+
82
+ | D-14
83
+ | oga
84
+ | Processing-instruction content leaks into text reads (oga's own
85
+ `text` folds PIs; the model says only text nodes contribute).
86
+ Surfaced by the taxonomy differential. Pinned in
87
+ `spec/consistency/differential_taxonomy_spec.rb`.
88
+
89
+ | D-15
90
+ | rexml
91
+ | The whitespace text node between a pre-root PI and the root is
92
+ retained (other adapters drop it). Surfaced by the taxonomy
93
+ differential. Pinned in `spec/consistency/differential_taxonomy_spec.rb`.
94
+
95
+ | D-16
96
+ | rexml
97
+ | Entity-expansion guard caps huge documents (~10k expansions) and
98
+ raised a bare engine error — the facade now names it a ParseError;
99
+ the cap itself remains (nokogiri's HUGE lane has no such cap).
100
+ Surfaced by the taxonomy differential. Pinned in
101
+ `spec/consistency/differential_taxonomy_spec.rb`.
102
+
103
+ | D-17
104
+ | oga
105
+ | The strict parse lane does not validate nesting well-formedness
106
+ (oga's strict is lexer-strict, not well-formedness-strict); bad
107
+ nesting parses where other adapters raise. Surfaced by the taxonomy
108
+ differential. Pinned in
109
+ `spec/consistency/differential_taxonomy_spec.rb`.
110
+
111
+ | D-18
112
+ | leptris, oga, ox, headed_ox
113
+ | Strict parse accepts invalid UTF-8 byte sequences (no encoding
114
+ validation at parse; the bytes flow through as binary). The
115
+ validating adapters (nokogiri, rexml, libxml) raise the named
116
+ ParseError. On leptris the facade raises the named ParseError in the
117
+ strict lane (default-UTF-8 rule; prolog-declared non-UTF-8 encodings
118
+ own their validity) pending engine-side validation —
119
+ leptris-ruby#514. Surfaced by the taxonomy differential. Pinned in
120
+ `spec/consistency/differential_taxonomy_spec.rb`.
121
+
122
+ | D-19
123
+ | nokogiri, libxml
124
+ | Adjacent text-like leaves (CDATA next to CDATA or text) coalesce
125
+ into a single text node on the libxml2 lane; oga, rexml, ox,
126
+ headed_ox, and leptris keep them as distinct nodes. The `text`
127
+ answer is identical — the node count and kinds differ. The model is
128
+ silent on text-node coalescing (an open decision). Surfaced by the
129
+ generated metamorphic differential (M5). Pinned in
130
+ `spec/consistency/metamorphic_properties_spec.rb` through
131
+ `InputGenerator::FEATURE_EXEMPT`.
132
+
133
+ | D-20
134
+ | rexml
135
+ | Trailing whitespace in processing-instruction data is dropped at
136
+ parse (the engine's instruction content is trimmed); leading
137
+ separator runs collapse per the model. Pinned in the taxonomy
138
+ differential (`pi trailing whitespace`, face `pi_content`).
90
139
  |===
@@ -0,0 +1,62 @@
1
+ ---
2
+ title: Ecosystem
3
+ nav_order: 12
4
+ ---
5
+
6
+ == Ecosystem closure
7
+
8
+ Moxml's consumer test: **no consumer needs to know which engine is
9
+ running, and every consumer suite runs against moxml main in this
10
+ repository's CI.** The dependent matrix
11
+ (`.github/workflows/dependent-tests.yml`, repo list in
12
+ `dependent-repos.json`) binds each consumer to the checkout at the
13
+ workflow's ref and runs its own suite — the consumer's suite is the
14
+ validation target, never a stand-in.
15
+
16
+ === Dependent matrix
17
+
18
+ [cols="2,1,3",options="header"]
19
+ |===
20
+ | Repository | Tools | Suite (validated 2026-10-21)
21
+
22
+ | `lutaml/lutaml-model` | — | 6213+ examples green on the released pair
23
+
24
+ | `lutaml/canon` | — | 2573 examples, 0 failures locally; in CI the Windows leg fails one JSON-engine parity example (`yaml_engine_parity_spec.rb:123`, the resolved json gem's duplicate-key verdict) — unrelated to XML processing, owner-held in canon
25
+
26
+ | `metanorma/metanorma-standoc` | ghostscript | 627 examples green in CI on main (the boilerplate-fixture failure seen during the 04/05 validation was resolved by the 06 adapter fixes; a few local-only failures still need generated assets)
27
+
28
+ | `metanorma/metanorma-ietf` | xml2rfc | 405 examples, 0 failures
29
+
30
+ | `lutaml/expressir` | — | 2026 examples, 0 failures
31
+
32
+ | `metanorma/svg_conform` | — | 350 examples, 0 failures
33
+ |===
34
+
35
+ === Seam audit
36
+
37
+ An *engine-direct seam* is consumer code that reaches past the facade
38
+ into the engine. Each is retired with a facade capability, or kept as
39
+ a declared, decided lane.
40
+
41
+ [cols="1,2,3",options="header"]
42
+ |===
43
+ | Consumer | Use | Disposition
44
+
45
+ | canon | HTML5 parse-lint (`parse_html(collect_errors:)`) | **Facade capability shipped**: `Moxml::Context#parse_html` forwards `collect_errors` and `Document#parse_diagnostics` answers the §13.2.5 token stream with code/line/column. canon adopts it in canon#281, which is owner-held (moxml does not merge canon PRs).
46
+
47
+ | canon | c14n native delegation (`doc.native.c14n`) | **Decided lane**: canon owns the c14n reference implementation and byte-probes the engine against its Ruby path; it is the backend behind moxml's own native delegation.
48
+
49
+ | metanorma-standoc | RELAX NG validation (`Leptris::XML::RelaxNG`) | **Facade capability added**: `Moxml::RelaxNG` (`Context#relaxng` / `#relaxng_file`, `valid?` / `validate` / `validate_errors`), loud `NotImplementedError` on engines without an RNG validator. Consumer migration tracked in standoc.
50
+
51
+ | svg_conform | engine parsing and SAX (`Leptris::XML.parse`, `Leptris::XML::SAX`) | **Consumer work**: moxml's `Context#parse` and `Moxml::SAX` cover both; tracked for the migration.
52
+
53
+ | standoc / svg_conform | hard adapter selection (`Moxml.new(:leptris)`) | **Supported**: adapter selection is the deployment decision the facade exists for.
54
+ |===
55
+
56
+ === Cap policy
57
+
58
+ A consumer pin (for example `moxml ~> 0.5.84`) is a dependency
59
+ constraint, not a knowledge of the engine. Caps move only after the
60
+ consumer's own suite runs green against the released pair — the
61
+ relaton-only lesson: evidence from one dependent suite does not
62
+ transfer to another.
@@ -571,4 +571,4 @@ HeadedOx v1.2 successfully delivers on its core promise: **fast XML parsing with
571
571
  - Heavy DOM modifications required
572
572
  - 100% feature parity is critical
573
573
 
574
- The documented limitations are transparent, well-understood, and unlikely to affect most XML processing workflows. HeadedOx fills an important niche in the Ruby XML ecosystem as the "fast XPath" option.
574
+ The known limitations are transparent, well-understood, and unlikely to affect most XML processing workflows. HeadedOx fills an important niche in the Ruby XML ecosystem as the "fast XPath" option.
@@ -658,7 +658,7 @@ following = items[current_index + 1..-1]
658
658
  ----
659
659
  Total Tests: 2,008
660
660
  Passing: 1,992 (99.20%)
661
- Skipped: 16 (0.80% - documented Ox limitations)
661
+ Skipped: 16 (0.80% - known Ox limitations; see link:divergences.html[the register])
662
662
  Failures: 0
663
663
 
664
664
  Test Categories:
@@ -686,7 +686,7 @@ Production Use: Ready
686
686
 
687
687
  === Known limitations with test status
688
688
 
689
- All 16 limitations are comprehensively documented:
689
+ All 16 limitations are catalogued below:
690
690
 
691
691
  . **Attribute wildcards** (3 tests) - Ox `locate()` doesn't support `@*`
692
692
  . **Namespace introspection** (4 tests) - Ox doesn't expose namespace data
@@ -8,6 +8,10 @@ nav_order: 1
8
8
  This section covers the fundamental concepts and essential information for
9
9
  working with Moxml.
10
10
 
11
+ Start with link:why-moxml[Why Moxml] for the invariant and the evidence
12
+ apparatus, then link:model-coverage[Model coverage] for the
13
+ adapter-by-adapter proof.
14
+
11
15
  === Topics covered
12
16
 
13
17
  link:installation[Installation]::
@@ -0,0 +1,57 @@
1
+ ---
2
+ title: Model coverage
3
+ nav_order: 5
4
+ ---
5
+
6
+ == Model coverage
7
+
8
+ Every face family of the
9
+ link:../_references/processing-model[processing model] is governed on
10
+ every adapter: **equivalent** by contract, **loud** (a named raise), or
11
+ a **registered divergence**. The table below is generated from the
12
+ coverage matrix that the consistency audit enforces, so the published
13
+ proof cannot drift from the tested proof.
14
+
15
+ The evidence chain is checkable at every link: the model section names
16
+ the specs that enforce it (`Pins::`), each enforcing spec cites the
17
+ model section, the matrix maps every face family to its section and
18
+ pin, and the divergence IDs resolve in
19
+ link:divergences.html[the register].
20
+
21
+ // MODEL-COVERAGE:BEGIN
22
+ Generated by `bundle exec rake docs:coverage` from the coverage matrix and the divergence register. Do not edit by hand: `spec/consistency/published_docs_spec.rb` fails when this block is stale.
23
+
24
+ [cols="4,1,1,1,1,1,1,1",options="header"]
25
+ |===
26
+ | Face family | nokogiri | oga | rexml | ox | headed ox | libxml | leptris
27
+ | document order and children listing | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
28
+ | name resolution | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
29
+ | namespace resolution and bindings normalization | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
30
+ | attribute access | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
31
+ | text/content/raw_content | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
32
+ | parsing modes and diagnostics | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
33
+ | EOL normalization | equivalent | D-02 | equivalent | D-03 | equivalent | equivalent | equivalent
34
+ | entity modes | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
35
+ | XPath evaluation and declared idioms | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
36
+ | serialization shapes (xml/html/xhtml) | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
37
+ | indentation knobs | equivalent | D-09 | equivalent | D-09 | D-09 | D-09 | D-09
38
+ | canonicalization | equivalent | equivalent | equivalent | D-05 | D-05 | equivalent | equivalent
39
+ | XSLT | equivalent | loud (NotImplementedError) | loud (NotImplementedError) | loud (NotImplementedError) | loud (NotImplementedError) | loud (NotImplementedError) | equivalent
40
+ | SAX | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
41
+ | materialization | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
42
+ | loud capability contracts | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
43
+ |===
44
+
45
+ *equivalent* — all adapters answer the same observable result. *loud (...)* — the adapter raises the named error for that face. *D-NN* — a registered divergence in link:divergences.html[the register], with its rationale. Every cell cites a pin; see the processing model's `Pins::` line for the enforcing specs.
46
+ // MODEL-COVERAGE:END
47
+
48
+ == Reproducing
49
+
50
+ `bundle exec rake docs:coverage` regenerates the block above from
51
+ `spec/support/model_coverage.rb`. `spec/consistency/published_docs_spec.rb`
52
+ fails when the published block is stale; the consistency suite runs it
53
+ on every CI build.
54
+
55
+ The performance commitment has its own published page:
56
+ link:performance.html[Performance], regenerated from the scoreboard
57
+ harness at release time.
@@ -5,6 +5,35 @@ nav_order: 11
5
5
 
6
6
  == Performance considerations
7
7
 
8
+ === Scoreboard (generated)
9
+
10
+ `SCORE_PUBLISH=1 bundle exec ruby benchmark/scoreboard.rb` rewrites
11
+ the marked block below from a live measurement; see
12
+ `benchmark/README.md` for the protocol and row definitions.
13
+
14
+ // SCOREBOARD:BEGIN
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
+
17
+ Measurement: Ruby 3.4.8, leptris 1.9.374.0, nokogiri 1.19.4, 2026-10-20T14:53:54Z, 1-minute load 34.01, 9 repetitions.
18
+
19
+ [cols="2,1,1,1",options="header"]
20
+ |===
21
+ | Row | Ratio | Kind | Status
22
+ | `parse` | 2.59x | parity | met
23
+ | `serialize` | 1.35x | parity | met
24
+ | `xpath //field (count)` | 1.20x | parity | met
25
+ | `xpath //field (each)` | 0.34x | structural | tracked (below parity)
26
+ | `attr pairs listing` | 8.16x | parity | met
27
+ | `build add_element` | 0.60x | structural | tracked (below parity)
28
+ | `walk+read (warm)` | 0.77x | structural | tracked (below parity)
29
+ | `walk+read (cold)` | 0.22x | structural | tracked (below parity)
30
+ | `children recursion (cold)` | 0.27x | structural | tracked (below parity)
31
+ | `plan records (typed)` | 0.79x | data | informational
32
+ |===
33
+
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.
35
+ // SCOREBOARD:END
36
+
8
37
  === Memory management
9
38
 
10
39
  Moxml maintains a node registry to ensure consistent object mapping:
@@ -0,0 +1,87 @@
1
+ ---
2
+ title: Why Moxml
3
+ nav_order: 2
4
+ ---
5
+
6
+ == Why Moxml
7
+
8
+ Moxml exists to make **"which XML engine am I running?" a deployment
9
+ decision instead of an API decision**.
10
+
11
+ Ruby's XML engines — Nokogiri, Oga, REXML, Ox, LibXML, Leptris — each
12
+ carry their own object model, namespace resolution, XPath coverage,
13
+ entity policy, serialization shapes, and error surfaces. An application
14
+ that starts on one cannot move to another without a rewrite; a library
15
+ that supports several accumulates adapter-specific branches and tests.
16
+ Moxml replaces that with a single processing model, specified once and
17
+ enforced on every adapter.
18
+
19
+ == The invariant
20
+
21
+ **Engine-choice invariance:** any program written against the Moxml
22
+ facade produces the same observable outcome on every adapter.
23
+ Performance may vary across engines — semantics may not.
24
+
25
+ Three clauses govern every change:
26
+
27
+ . **Equivalence is to the model, not merely adapter-to-adapter.** Two
28
+ engines can agree and both be wrong; the normative document is the
29
+ reference.
30
+ . **Loud degradation, never silent divergence.** An unsupported
31
+ capability raises a named error. A different answer is a bug.
32
+ . **A divergence is fixed, promoted to specification, or registered.**
33
+ The register names it, explains it, and pins the tests that observe
34
+ it. An undocumented divergence is a bug.
35
+
36
+ == How the claim is proved
37
+
38
+ The proof apparatus is itself part of the project, and every link is
39
+ checked in CI:
40
+
41
+ [cols="1,3"]
42
+ |===
43
+ | Link | What it establishes
44
+
45
+ | link:../_references/processing-model[Processing model]
46
+ | The single normative statement of every semantic decision, with a
47
+ `Pins::` line naming the specs that enforce each section.
48
+
49
+ | link:model-coverage.html[Model coverage]
50
+ | The face-family × adapter matrix: equivalent, loud, or a registered
51
+ divergence. Generated from the same data the consistency audit
52
+ enforces.
53
+
54
+ | link:divergences.html[Divergence register]
55
+ | Every known difference, with its engine cause, its scope, and its
56
+ pin. Entries are fixed, loud, or decided permanent trades.
57
+
58
+ | link:performance.html[Performance]
59
+ | The published scoreboard: parity rows are commitments; structural
60
+ gaps are measured and trend-locked, never hidden.
61
+
62
+ |===
63
+
64
+ Behind the pages, the consistency suite runs an engine-free
65
+ **executable reference** (a pure-Ruby reader, writer, and normalizer)
66
+ against every adapter; a seeded **generator** checks metamorphic
67
+ properties over generated documents; and a whole-class **taxonomy
68
+ differential** compares every adapter on every input class the model
69
+ touches. The reference is itself transpiled to JavaScript and runs on
70
+ the Opal lane.
71
+
72
+ == What Moxml is not
73
+
74
+ Moxml is not a new XML parser and does not replace the engines — it
75
+ makes them interchangeable. It is not a performance claims contest
76
+ either: performance is a declared, non-semantic channel, and the
77
+ scoreboard reports below-parity rows as plainly as parity rows.
78
+
79
+ == The consumer commitment
80
+
81
+ - Upgrading an engine version never changes your program's semantics.
82
+ - Capabilities that an engine lacks raise a named error at the call
83
+ site, not a surprising result.
84
+ - Every difference you can observe has a register ID and a test.
85
+ - The same program runs on Nokogiri, Oga, REXML, Ox, LibXML, and
86
+ Leptris — and on a future engine that adopts the model with zero
87
+ code changes.
@@ -17,15 +17,30 @@ skip with their entry ID — an undocumented divergence is a bug.
17
17
 
18
18
  === Status and precedence
19
19
 
20
+ Pins:: none
21
+
20
22
  1. This document — the model.
21
- 2. `spec/consistency/` — the model pinned as executable expectations.
23
+ 2. `spec/consistency/` — the model pinned as executable expectations,
24
+ with `lib/moxml/reference/` as the **engine-free executable
25
+ reference** (a pure-Ruby reader, writer, and normalizer). Adapters
26
+ are differentially tested against the reference
27
+ (`spec/consistency/reference_differential_spec.rb`), not only
28
+ against each other; the reference is pure Ruby and transpiles to
29
+ the Opal lane.
22
30
  3. Adapter implementations and capability gates — representations.
23
31
 
32
+ A pin cites the model section it enforces
33
+ (`# Processing model §<section> (normative)`); the coverage audit
34
+ asserts both directions of that linkage, so removing any of the
35
+ document, a pin, or the reference fails CI.
36
+
24
37
  A semantics change amends this document first (see
25
38
  `TODO.model/05-model-evolution-protocol.md`).
26
39
 
27
40
  === Mission and invariant
28
41
 
42
+ Pins:: none
43
+
29
44
  Moxml exists to make "which XML engine am I running?" a **deployment
30
45
  decision instead of an API decision**. The invariant that enables it:
31
46
  any program written against the facade produces the same observable
@@ -39,6 +54,8 @@ divergences, and performance.
39
54
 
40
55
  === Object model
41
56
 
57
+ Pins:: none
58
+
42
59
  Node kinds:: `Document`, `Element`, `Attribute`, `Text`, `Comment`,
43
60
  `Cdata`, `Declaration`, `Doctype`, `ProcessingInstruction`,
44
61
  `Namespace`, `EntityReference`.
@@ -48,6 +65,12 @@ the same underlying node; node identity is a property of the
48
65
  underlying tree, not of wrapper object identity. Comparisons use node
49
66
  equality semantics, never `equal?` on wrappers.
50
67
 
68
+ Kind:: Every node answers `node_type` — one of `:document`,
69
+ `:element`, `:attribute`, `:text`, `:comment`, `:cdata`,
70
+ `:processing_instruction`, `:doctype`, `:declaration`,
71
+ `:entity_reference`, `:namespace` — the predicates (`element?`,
72
+ `text?`, …) are its derivatives.
73
+
51
74
  Ownership:: The `Document` owns the tree's lifetime; nodes are valid
52
75
  within it. Deterministic release is available via `Document#free`.
53
76
 
@@ -57,15 +80,23 @@ listings or serialization child walks.
57
80
 
58
81
  === Document order and traversal
59
82
 
83
+ Pins:: `spec/consistency/adapter_parity_spec.rb`
84
+
60
85
  * `children` lists nodes in document order.
61
86
  * Walks (`each_node`, `materialize`) visit depth-first, pre-order.
62
87
  * `Document#children` and `#root` never include the declaration;
63
88
  doctype exposure follows the Nokogiri contract.
89
+ * Document-level parts (`Declaration`, `Doctype`) are state, not
90
+ nodes: walks (`traverse`, `each_node`, `materialize`) never yield
91
+ them, and walks over a DOCTYPE-bearing document are well-defined on
92
+ every adapter.
64
93
  * `NodeSet` preserves document order for axis results; unions preserve
65
94
  first-encounter document order.
66
95
 
67
96
  === Names and namespaces
68
97
 
98
+ Pins:: `spec/consistency/xpath_idioms_spec.rb`
99
+
69
100
  * Names are **expanded names**: a name test matches by namespace *URI*,
70
101
  never by prefix spelling.
71
102
  * Unprefixed element name tests match **no-namespace** elements only
@@ -81,12 +112,16 @@ listings or serialization child walks.
81
112
 
82
113
  === Access semantics
83
114
 
115
+ Pins:: `spec/moxml/attribute_resolver_spec.rb`, `spec/moxml/text_spec.rb`
116
+
84
117
  `text`:: Concatenated descendant text (elements); a text node's own
85
118
  content; entity references restored when the document is
86
119
  entity-bearing.
87
120
 
88
121
  `content`:: The node's content per its kind; on text-like nodes, with
89
- entity-marker restoration; memoized against mutation.
122
+ entity-marker restoration; memoized against mutation. A processing
123
+ instruction's content is its data **without** the separating
124
+ whitespace after the target (uniform across adapters).
90
125
 
91
126
  `raw_content`:: The backing text **without** entity restoration.
92
127
 
@@ -103,6 +138,8 @@ with an unambiguous local name.
103
138
 
104
139
  === Parsing
105
140
 
141
+ Pins:: `spec/consistency/eol_normalization_spec.rb`, `spec/moxml/document_parse_diagnostics_spec.rb`, `spec/moxml/entity_spec.rb`
142
+
106
143
  * Modes: strict (errors raise) and recover (errors surface via
107
144
  `parse_diagnostics` / `parse_errors`, both `[{kind:, message:}]`).
108
145
  * HUGE mode: over-cap text (> 10 MB) parses completely.
@@ -116,6 +153,8 @@ with an unambiguous local name.
116
153
 
117
154
  === XPath evaluation
118
155
 
156
+ Pins:: `spec/consistency/xpath_idioms_spec.rb`
157
+
119
158
  * XPath 1.0 semantics on every adapter. Engines without an evaluator
120
159
  (Ox, HeadedOx; leptris for shapes its engine cannot evaluate) run
121
160
  **moxml's own pure-Ruby engine** — the model's semantics do not
@@ -128,16 +167,25 @@ with an unambiguous local name.
128
167
  `xmlns:` default-namespace matching; attribute terminals in every
129
168
  position (`//a/@id`, `//@id`, `r/a/@id`); symbol- and string-keyed
130
169
  bindings resolve identically.
170
+ * `at_xpath` answers the first node of a node-set and passes scalar
171
+ results (`count()`, `string()`, `boolean()`) through unwrapped,
172
+ uniformly across adapters. Adapters whose at_xpath lane falls back
173
+ to the reference engine when the native answer is empty (leptris)
174
+ use it; the rest evaluate `xpath` and take the first node. Both
175
+ lanes answer the same model.
131
176
 
132
177
  === Serialization
133
178
 
179
+ Pins:: `spec/moxml/to_html_spec.rb`, `spec/moxml/xhtml_output_spec.rb`
180
+
134
181
  [cols="1,3"]
135
182
  |===
136
183
  | Shape | Contract
137
184
 
138
185
  | `to_xml`
139
186
  | Declaration per configuration; empty elements per `expand_empty`;
140
- namespace declarations preserved.
187
+ namespace declarations preserved; a parsed DOCTYPE is emitted with
188
+ its external identifiers and internal subset.
141
189
 
142
190
  | `to_html`
143
191
  | HTML method: void elements unclosed (`<br>`), raw text
@@ -161,15 +209,25 @@ with an unambiguous local name.
161
209
 
162
210
  === Canonicalization
163
211
 
212
+ Pins:: `spec/moxml/c14n/adapter_delegation_spec.rb`
213
+
164
214
  * Algorithms: Inclusive 1.0, Exclusive 1.0, Inclusive 1.1, plus
165
215
  XPath-driven subset canonicalization (enveloped-signature oracle).
166
216
  * Native delegations (leptris engine, libxml2) answer **byte-identical**
167
217
  output to the Ruby reference path (byte-safety probed before
168
218
  engagement).
219
+ * Document-level comments and PIs render in document order, separated
220
+ from adjacent nodes by exactly one line feed; the form starts and
221
+ ends with its first/last node (no leading or trailing line feed).
222
+ The XML declaration is document state and never part of the form.
223
+ * PI data excludes the separating whitespace after the target; the
224
+ canonical form joins target and non-empty data with a single space.
169
225
  * `Moxml::C14n.equivalent?` is the algorithm-aware comparison.
170
226
 
171
227
  === XSLT
172
228
 
229
+ Pins:: `spec/consistency/xslt_spec.rb`
230
+
173
231
  XSLT 1.0 through engines that ship a processor (libxslt; leptris
174
232
  1.0–3.0). Compilation and transform failures raise named
175
233
  `Moxml::XsltError`s. Engines without a processor raise
@@ -177,6 +235,8 @@ XSLT 1.0 through engines that ship a processor (libxslt; leptris
177
235
 
178
236
  === SAX
179
237
 
238
+ Pins:: `spec/moxml/sax_spec.rb`
239
+
180
240
  * The callback stream (`on_start_element`, `on_characters`, `on_error`,
181
241
  …) is the authoritative event sequence.
182
242
  * The record-drain fast path (`on_sax_records`) replays **exactly** the
@@ -186,6 +246,8 @@ XSLT 1.0 through engines that ship a processor (libxslt; leptris
186
246
 
187
247
  === Materialization
188
248
 
249
+ Pins:: `spec/moxml/plan_conformance_spec.rb`
250
+
189
251
  `materialize`, `materialize_fields`, `Plan`, and `StructPlan` produce
190
252
  **identical records across all adapters** (conformance spec). Plan name
191
253
  matching is global; unmatched subtrees are dropped per the pinned
@@ -193,6 +255,8 @@ semantics.
193
255
 
194
256
  === Errors and loud degradation
195
257
 
258
+ Pins:: `spec/consistency/loud_degradation_spec.rb`
259
+
196
260
  * `Moxml::Error` hierarchy; `Moxml::XPathError`, `Moxml::XsltError`,
197
261
  `Moxml::AdapterError` (e.g. schema-handle/adapter misroute),
198
262
  `Moxml::NotImplementedError` for unsupported capability contracts.
@@ -204,6 +268,8 @@ semantics.
204
268
 
205
269
  === Performance: a declared, non-semantic channel
206
270
 
271
+ Pins:: none
272
+
207
273
  Acceleration (native read layers, C minting, record drains, plan
208
274
  streams) always lands behind version + face gates and degrades to the
209
275
  spec-pinned fallback. Cross-adapter performance variance is sanctioned
@@ -62,3 +62,7 @@ require "moxml/xpath/errors"
62
62
  require "moxml/xpath/ast/node"
63
63
  require "moxml/xpath/ruby/node"
64
64
  require "moxml/xpath/ruby/generator"
65
+ require "moxml/reference"
66
+ require "moxml/reference/parser"
67
+ require "moxml/reference/writer"
68
+ require "moxml/reference/normalizer"