moxml 0.5.149 → 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 (61) 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/performance-release.yml +79 -0
  5. data/.github/workflows/performance.yml +59 -0
  6. data/Rakefile +23 -0
  7. data/benchmark/README.md +106 -0
  8. data/benchmark/scoreboard.json +115 -0
  9. data/benchmark/scoreboard.rb +556 -0
  10. data/bin/generate-model-coverage +21 -0
  11. data/docs/_pages/adapter-compatibility.adoc +3 -3
  12. data/docs/_pages/divergences.adoc +32 -41
  13. data/docs/_pages/ecosystem.adoc +62 -0
  14. data/docs/_pages/headed-ox-limitations.adoc +1 -1
  15. data/docs/_pages/headed-ox.adoc +2 -2
  16. data/docs/_pages/index.adoc +4 -0
  17. data/docs/_pages/model-coverage.adoc +57 -0
  18. data/docs/_pages/performance.adoc +29 -0
  19. data/docs/_pages/why-moxml.adoc +87 -0
  20. data/docs/_references/processing-model.adoc +42 -3
  21. data/lib/moxml/adapter/base.rb +73 -0
  22. data/lib/moxml/adapter/leptris.rb +183 -4
  23. data/lib/moxml/adapter/libxml.rb +34 -2
  24. data/lib/moxml/adapter/nokogiri.rb +10 -0
  25. data/lib/moxml/adapter/oga.rb +39 -7
  26. data/lib/moxml/adapter/ox.rb +8 -0
  27. data/lib/moxml/adapter/rexml.rb +17 -2
  28. data/lib/moxml/c14n/data_model.rb +8 -0
  29. data/lib/moxml/c14n/processor.rb +17 -8
  30. data/lib/moxml/context.rb +28 -0
  31. data/lib/moxml/document.rb +17 -3
  32. data/lib/moxml/node.rb +56 -5
  33. data/lib/moxml/node_set.rb +31 -1
  34. data/lib/moxml/relaxng.rb +62 -0
  35. data/lib/moxml/sax/block_handler.rb +7 -0
  36. data/lib/moxml/sax/handler.rb +20 -0
  37. data/lib/moxml/version.rb +1 -1
  38. data/lib/moxml/xpath.rb +28 -0
  39. data/lib/moxml.rb +1 -0
  40. data/spec/consistency/adapter_parity_spec.rb +22 -0
  41. data/spec/consistency/citation_hygiene_spec.rb +30 -0
  42. data/spec/consistency/model_coverage_audit_spec.rb +61 -86
  43. data/spec/consistency/published_docs_spec.rb +49 -0
  44. data/spec/consistency/xpath_idioms_spec.rb +23 -5
  45. data/spec/integration/shared_examples/edge_cases.rb +0 -13
  46. data/spec/moxml/adapter/leptris_spec.rb +23 -3
  47. data/spec/moxml/adapter/shared_examples/adapter_contract.rb +0 -8
  48. data/spec/moxml/attribute_resolver_spec.rb +33 -0
  49. data/spec/moxml/c14n/adapter_delegation_spec.rb +33 -6
  50. data/spec/moxml/c14n/character_encoding_spec.rb +7 -13
  51. data/spec/moxml/c14n/comments_pis_spec.rb +35 -8
  52. data/spec/moxml/c14n/subset_spec.rb +2 -12
  53. data/spec/moxml/c14n/w3c_examples_spec.rb +2 -5
  54. data/spec/moxml/element_indent_spec.rb +34 -0
  55. data/spec/moxml/node_spec.rb +0 -3
  56. data/spec/moxml/relax_ng_spec.rb +67 -0
  57. data/spec/moxml/sax_spec.rb +45 -74
  58. data/spec/moxml/xpath_capabilities_spec.rb +11 -38
  59. data/spec/support/input_taxonomy.rb +7 -0
  60. data/spec/support/model_coverage.rb +159 -0
  61. metadata +17 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3af023098a8dde5521f0297f240a1c6fa71625cc2b3cd6924eba638724e12517
4
- data.tar.gz: 9fb194d059475ed0c4aea3471b9048195ce7ea0bef349c25bdc52f991944ca5f
3
+ metadata.gz: 73b67b633933c60095bdeafd744a8f7a827066edf478245e9c2f49aadf1d75e6
4
+ data.tar.gz: 92320b13e8bb745cd5b650e85d4fa7b8397e082871a7c255cb61f637595c6048
5
5
  SHA512:
6
- metadata.gz: 860ac49f2684d504273e22dcbc9c6ed43106ebf67d9ca093af74543ab284e89c7be5c02d3991421973674837ece981127ede74329bfce9a352236bf9b3c094ff
7
- data.tar.gz: 24b50dee831eb6856dc3f24b7759e32aead60c98896427ed7776277cd69aca0db01f3521841cbc556ccfaaee8fca56a071f2f219276f0553fe919b712c87ef2b
6
+ metadata.gz: c34b4df4cf09797b5a28ec909e0df41c970897676154fa1b66dc9ebbd18cdccb13408b83f5647c44d41cfda7ebcbf09571314d0bfe71b572a58dde972e78994b
7
+ data.tar.gz: d926c442a8b0dc0f9d159f27f42772a3af48358fbbd67d62d7cec1cdd51c609110def03466fa93c5b95b2eccfdaa7ab34e26c818b30b2651a996b9879c63d699
@@ -1,5 +1,10 @@
1
1
  {
2
2
  "repo": [
3
- "lutaml/lutaml-model"
3
+ "lutaml/lutaml-model",
4
+ "lutaml/canon",
5
+ "metanorma/metanorma-standoc",
6
+ "metanorma/metanorma-ietf",
7
+ "lutaml/expressir",
8
+ "metanorma/svg_conform"
4
9
  ]
5
- }
10
+ }
@@ -24,4 +24,7 @@ jobs:
24
24
  rake:
25
25
  uses: metanorma/ci/.github/workflows/dependent-rake.yml@main
26
26
  with:
27
- command: bundle exec rspec
27
+ command: bundle exec rspec
28
+ # metanorma-ietf needs xml2rfc; metanorma-standoc needs
29
+ # ghostscript. Both are harmless for the other consumers.
30
+ setup-tools: xml2rfc,ghostscript
@@ -0,0 +1,79 @@
1
+ name: performance-release
2
+
3
+ # Every release re-measures the scoreboard and appends the table to
4
+ # the release notes (TODO.sota/04). Triggered by the release
5
+ # workflow's completion; idempotent — a re-run replaces the marked
6
+ # section instead of stacking it.
7
+ on:
8
+ workflow_run:
9
+ workflows: ["release"]
10
+ types: [completed]
11
+ workflow_dispatch:
12
+
13
+ concurrency:
14
+ group: ${{ github.workflow }}
15
+ cancel-in-progress: true
16
+
17
+ permissions:
18
+ contents: write
19
+
20
+ jobs:
21
+ report:
22
+ if: >-
23
+ github.event_name == 'workflow_dispatch' ||
24
+ github.event.workflow_run.conclusion == 'success'
25
+ runs-on: ubuntu-latest
26
+ steps:
27
+ - uses: actions/checkout@v6
28
+ with:
29
+ ref: main
30
+ submodules: "recursive"
31
+
32
+ - name: Set up Ruby
33
+ uses: ruby/setup-ruby@v1
34
+ with:
35
+ ruby-version: "3.3"
36
+ bundler-cache: false
37
+
38
+ - name: Install ragel
39
+ run: sudo apt-get update && sudo apt-get install -y ragel
40
+
41
+ - name: Install ruby-ll (for rake vendor:prepare)
42
+ run: gem install ruby-ll --no-document
43
+
44
+ - name: Generate ragel / ruby-ll outputs in vendored forks
45
+ run: rake vendor:prepare
46
+
47
+ - name: Install dependencies
48
+ run: bundle install
49
+
50
+ - name: Compile liboga / libll native extensions
51
+ run: rake vendor:compile
52
+
53
+ - name: Measure the scoreboard
54
+ run: |
55
+ SCORE_MARKDOWN=/tmp/section.md \
56
+ bundle exec ruby benchmark/scoreboard.rb
57
+
58
+ - name: Append the scoreboard to the latest release notes
59
+ env:
60
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
61
+ run: |
62
+ TAG=$(gh release view --json tagName --jq .tagName)
63
+ gh release view "$TAG" --json body --jq .body > /tmp/body.md
64
+ ruby -e '
65
+ body = File.read("/tmp/body.md")
66
+ section = File.read("/tmp/section.md")
67
+ block = "<!-- PERFORMANCE:BEGIN -->\n#{section}" \
68
+ "<!-- PERFORMANCE:END -->"
69
+ if body.include?("<!-- PERFORMANCE:BEGIN -->")
70
+ body = body.sub(
71
+ /<!-- PERFORMANCE:BEGIN -->.*<!-- PERFORMANCE:END -->/m,
72
+ block,
73
+ )
74
+ else
75
+ body = body.chomp + "\n\n" + block
76
+ end
77
+ File.write("/tmp/new_body.md", body)
78
+ '
79
+ gh release edit "$TAG" --notes-file /tmp/new_body.md
@@ -0,0 +1,59 @@
1
+ name: performance
2
+
3
+ # The published scoreboard lock (TODO.sota/04). Runs on a quiet
4
+ # weekly schedule (and on demand); PRs do not run it — measurement
5
+ # noise is a release concern, not a change-gating one.
6
+ on:
7
+ schedule:
8
+ - cron: "0 4 * * 1"
9
+ workflow_dispatch:
10
+
11
+ concurrency:
12
+ group: ${{ github.workflow }}-${{ github.ref }}
13
+ cancel-in-progress: true
14
+
15
+ permissions:
16
+ contents: read
17
+
18
+ jobs:
19
+ lock:
20
+ runs-on: ubuntu-latest
21
+ steps:
22
+ - uses: actions/checkout@v6
23
+ with:
24
+ submodules: "recursive"
25
+
26
+ - name: Set up Ruby
27
+ uses: ruby/setup-ruby@v1
28
+ with:
29
+ ruby-version: "3.3"
30
+ # bundler-cache is disabled because the path-source oga and
31
+ # ruby-ll forks need generated outputs before their C
32
+ # extensions can compile.
33
+ bundler-cache: false
34
+
35
+ - name: Install ragel
36
+ run: sudo apt-get update && sudo apt-get install -y ragel
37
+
38
+ - name: Install ruby-ll (for rake vendor:prepare)
39
+ run: gem install ruby-ll --no-document
40
+
41
+ - name: Generate ragel / ruby-ll outputs in vendored forks
42
+ run: rake vendor:prepare
43
+
44
+ - name: Install dependencies
45
+ run: bundle install
46
+
47
+ - name: Compile liboga / libll native extensions
48
+ run: rake vendor:compile
49
+
50
+ - name: Scoreboard lock (parity rows + 10% regression tolerance)
51
+ run: bundle exec rake benchmark:lock
52
+
53
+ - name: Upload the measured scoreboard
54
+ if: always()
55
+ uses: actions/upload-artifact@v4
56
+ with:
57
+ name: scoreboard
58
+ path: benchmark/scoreboard.json
59
+ if-no-files-found: ignore
data/Rakefile CHANGED
@@ -284,6 +284,22 @@ namespace :benchmark do
284
284
  task :report do
285
285
  ruby "benchmarks/generate_report.rb"
286
286
  end
287
+
288
+ # Published scoreboard (TODO.sota/04): CPU medians, interleaved A/B,
289
+ # load-gated; see benchmark/README.md for the protocol and rows.
290
+ desc "Published performance scoreboard (CPU medians, interleaved A/B)"
291
+ task :scoreboard do
292
+ sh "bundle exec ruby benchmark/scoreboard.rb"
293
+ end
294
+
295
+ # CI lock: parity rows must be >= 1.0x and no row may fall more than
296
+ # 10% below the committed baseline.
297
+ desc "Scoreboard regression lock (parity + 10% baseline tolerance)"
298
+ task :lock do
299
+ sh({ "SCORE_LOCK" => "1",
300
+ "SCORE_BASELINE" => "benchmark/scoreboard.json" },
301
+ "bundle exec ruby benchmark/scoreboard.rb")
302
+ end
287
303
  end
288
304
 
289
305
  namespace :opal do
@@ -324,4 +340,11 @@ namespace :opal do
324
340
  end
325
341
  end
326
342
 
343
+ namespace :docs do
344
+ desc "Regenerate the published model-coverage page from the matrix"
345
+ task :coverage do
346
+ sh "bundle exec ruby -I spec bin/generate-model-coverage"
347
+ end
348
+ end
349
+
327
350
  task default: :spec
@@ -0,0 +1,106 @@
1
+ # Performance scoreboard
2
+
3
+ `benchmark/scoreboard.rb` is the published performance surface
4
+ (TODO.sota/04). It answers one question for every operation a
5
+ consumer runs: **how does moxml on its fastest engine lane compare
6
+ with raw nokogiri on the same operation?**
7
+
8
+ ## Protocol
9
+
10
+ - **CPU time medians** (`CLOCK_PROCESS_CPUTIME_ID`), not wall time —
11
+ scheduler noise never becomes a performance claim.
12
+ - **Interleaved A/B**: baseline and candidate alternate every
13
+ repetition in the same thermal window; warmups are discarded and
14
+ GC runs between samples. The ratio is `median(baseline) /
15
+ median(candidate)`; `> 1.0x` means moxml is faster.
16
+ - **Load gate**: the harness refuses to report when the 1-minute
17
+ load exceeds half the CPU count. `SCORE_FORCE=1` overrides for
18
+ smoke runs only — such numbers are not publishable.
19
+ - Rows name the exact operation measured; lazy and materialized
20
+ XPath results are separate rows because they cost different things.
21
+ - The pair is **moxml + leptris latest**; the baseline is raw
22
+ nokogiri on the same document.
23
+
24
+ ## Rows
25
+
26
+ | Row | Operation | Kind |
27
+ | --- | --------- | ---- |
28
+ | `parse` | `Nokogiri::XML` vs `ctx.parse` on the catalog document | parity |
29
+ | `serialize` | `doc.to_xml` | parity |
30
+ | `xpath //field (count)` | node-set cardinality, nothing materialized | parity |
31
+ | `xpath //field (each)` | iterate every result (wrapper materialization) | structural |
32
+ | `attr pairs listing` | collect `[name, value]` for every record | parity |
33
+ | `build add_element` | create + attach 300 records x 4 fields with attrs and text | structural |
34
+ | `walk+read (warm)` | traverse a pre-parsed document, read name + text | structural |
35
+ | `walk+read (cold)` | parse + the same traversal | structural |
36
+ | `children recursion (cold)` | parse + recurse via `children` filtering elements | structural |
37
+ | `plan records (typed)` | compiled `Moxml::Plan` vs the hand-written typed walk | data API |
38
+
39
+ **Parity rows** are the commitment: the lock fails below a 0.95
40
+ measurement floor. **Structural rows** are measured and trend-locked
41
+ (no row may regress >10% against the committed baseline), but they
42
+ are known to be below parity — the gaps are listed below. **Data
43
+ API** rows are informational: they show what consumers gain by using
44
+ the record APIs instead of node-by-node access.
45
+
46
+ ## Commands
47
+
48
+ ```bash
49
+ # Every row, human-readable
50
+ bundle exec rake benchmark:scoreboard
51
+
52
+ # One or more rows
53
+ SCORE_ROWS="parse,serialize" bundle exec ruby benchmark/scoreboard.rb
54
+
55
+ # Another engine lane
56
+ SCORE_ADAPTER=oga bundle exec ruby benchmark/scoreboard.rb
57
+
58
+ # Machine-readable results (protocol metadata + rows)
59
+ SCORE_JSON=benchmark/scoreboard.json bundle exec ruby benchmark/scoreboard.rb
60
+
61
+ # CI lock: parity rows >= 1.0x, and no row > 10% below the baseline
62
+ SCORE_LOCK=1 SCORE_BASELINE=benchmark/scoreboard.json \
63
+ bundle exec ruby benchmark/scoreboard.rb
64
+ ```
65
+
66
+ A smoke run on a loaded machine:
67
+
68
+ ```bash
69
+ SCORE_FORCE=1 SCORE_REPS=5 bundle exec ruby benchmark/scoreboard.rb
70
+ ```
71
+
72
+ ## Honest status
73
+
74
+ Reference run (generated; do not edit by hand — `SCORE_PUBLISH=1`
75
+ rewrites this block and `docs/_pages/performance.adoc`):
76
+
77
+ <!-- SCOREBOARD:BEGIN -->
78
+ | Row | Ratio | Kind | Status |
79
+ | --- | ----- | ---- | ------ |
80
+ | `parse` | 2.59x | parity | met |
81
+ | `serialize` | 1.35x | parity | met |
82
+ | `xpath //field (count)` | 1.20x | parity | met |
83
+ | `xpath //field (each)` | 0.34x | structural | tracked (below parity) |
84
+ | `attr pairs listing` | 8.16x | parity | met |
85
+ | `build add_element` | 0.60x | structural | tracked (below parity) |
86
+ | `walk+read (warm)` | 0.77x | structural | tracked (below parity) |
87
+ | `walk+read (cold)` | 0.22x | structural | tracked (below parity) |
88
+ | `children recursion (cold)` | 0.27x | structural | tracked (below parity) |
89
+ | `plan records (typed)` | 0.79x | data | informational |
90
+ <!-- SCOREBOARD:END -->
91
+
92
+ The open rows are the known structural gap: every node the facade
93
+ hands a consumer is a contract-carrying wrapper with its own
94
+ identity/invalidation state, while nokogiri hands out thin handles.
95
+ Closing them needs engine-side construction/walk faces (leptris
96
+ C contract work), not more facade tuning. Facade-side wins already
97
+ landed here: single-crossing `add_element` construction, native
98
+ `set_binding_text` without a binding-twin mint per write, and the
99
+ generation memo no longer writing on every `children` read.
100
+
101
+ Numbers move by a few percent between runs; the lock's 10% tolerance
102
+ is the regression tripwire, not a claim of precision. The committed
103
+ baseline is a reference measured on a shared machine — regenerate it
104
+ on a quiet machine (the `performance` workflow uploads its measured
105
+ `scoreboard.json` as an artifact) before treating a lock failure as
106
+ a regression.
@@ -0,0 +1,115 @@
1
+ {
2
+ "protocol": {
3
+ "measured": "2026-10-20T14:53:54Z",
4
+ "ruby": "3.4.8",
5
+ "adapter": "leptris",
6
+ "adapter_version": "1.9.374.0",
7
+ "nokogiri": "1.19.4",
8
+ "reps": 9,
9
+ "warmup": 2,
10
+ "clock": "CLOCK_PROCESS_CPUTIME_ID",
11
+ "load": 34.01
12
+ },
13
+ "rows": [
14
+ {
15
+ "row": "parse",
16
+ "kind": "parity",
17
+ "parity": true,
18
+ "baseline_us": 677.0,
19
+ "candidate_us": 261.0,
20
+ "ratio": 2.594,
21
+ "baseline_allocs": 6,
22
+ "candidate_allocs": 18
23
+ },
24
+ {
25
+ "row": "serialize",
26
+ "kind": "parity",
27
+ "parity": true,
28
+ "baseline_us": 474.0,
29
+ "candidate_us": 351.0,
30
+ "ratio": 1.35,
31
+ "baseline_allocs": 30,
32
+ "candidate_allocs": 13
33
+ },
34
+ {
35
+ "row": "xpath //field (count)",
36
+ "kind": "parity",
37
+ "parity": true,
38
+ "baseline_us": 132.0,
39
+ "candidate_us": 110.0,
40
+ "ratio": 1.2,
41
+ "baseline_allocs": 12,
42
+ "candidate_allocs": 9
43
+ },
44
+ {
45
+ "row": "xpath //field (each)",
46
+ "kind": "structural",
47
+ "parity": false,
48
+ "baseline_us": 276.0,
49
+ "candidate_us": 800.0,
50
+ "ratio": 0.345,
51
+ "baseline_allocs": 1212,
52
+ "candidate_allocs": 12
53
+ },
54
+ {
55
+ "row": "attr pairs listing",
56
+ "kind": "parity",
57
+ "parity": true,
58
+ "baseline_us": 310.0,
59
+ "candidate_us": 38.0,
60
+ "ratio": 8.158,
61
+ "baseline_allocs": 3300,
62
+ "candidate_allocs": 0
63
+ },
64
+ {
65
+ "row": "build add_element",
66
+ "kind": "structural",
67
+ "parity": false,
68
+ "baseline_us": 3434.0,
69
+ "candidate_us": 5744.0,
70
+ "ratio": 0.598,
71
+ "baseline_allocs": 20358,
72
+ "candidate_allocs": 11062
73
+ },
74
+ {
75
+ "row": "walk+read (warm)",
76
+ "kind": "structural",
77
+ "parity": false,
78
+ "baseline_us": 1101.0,
79
+ "candidate_us": 1431.0,
80
+ "ratio": 0.769,
81
+ "baseline_allocs": 4503,
82
+ "candidate_allocs": 2
83
+ },
84
+ {
85
+ "row": "walk+read (cold)",
86
+ "kind": "structural",
87
+ "parity": false,
88
+ "baseline_us": 1915.0,
89
+ "candidate_us": 8590.0,
90
+ "ratio": 0.223,
91
+ "baseline_allocs": 6010,
92
+ "candidate_allocs": 8730
93
+ },
94
+ {
95
+ "row": "children recursion (cold)",
96
+ "kind": "structural",
97
+ "parity": false,
98
+ "baseline_us": 2293.0,
99
+ "candidate_us": 8595.0,
100
+ "ratio": 0.267,
101
+ "baseline_allocs": 5710,
102
+ "candidate_allocs": 7230
103
+ },
104
+ {
105
+ "row": "plan records (typed)",
106
+ "kind": "data",
107
+ "parity": false,
108
+ "baseline_us": 2401.0,
109
+ "candidate_us": 3038.0,
110
+ "ratio": 0.79,
111
+ "baseline_allocs": 8111,
112
+ "candidate_allocs": 13231
113
+ }
114
+ ]
115
+ }