moxml 0.5.147 → 0.5.149

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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/nightly-fuzz.yml +80 -0
  3. data/docs/_pages/divergences.adoc +58 -0
  4. data/docs/_references/processing-model.adoc +30 -3
  5. data/lib/compat/opal/moxml_boot.rb +4 -0
  6. data/lib/moxml/adapter/leptris.rb +180 -20
  7. data/lib/moxml/adapter/libxml.rb +145 -23
  8. data/lib/moxml/adapter/oga.rb +33 -3
  9. data/lib/moxml/adapter/ox.rb +26 -2
  10. data/lib/moxml/adapter/rexml.rb +65 -15
  11. data/lib/moxml/context.rb +5 -1
  12. data/lib/moxml/document.rb +24 -10
  13. data/lib/moxml/node.rb +34 -6
  14. data/lib/moxml/node_set.rb +11 -0
  15. data/lib/moxml/reference/normalizer.rb +63 -0
  16. data/lib/moxml/reference/parser.rb +417 -0
  17. data/lib/moxml/reference/writer.rb +121 -0
  18. data/lib/moxml/reference.rb +205 -0
  19. data/lib/moxml/version.rb +1 -1
  20. data/lib/moxml/xpath/compiler.rb +17 -2
  21. data/lib/moxml/xpath/ordering.rb +97 -0
  22. data/lib/moxml/xpath.rb +1 -0
  23. data/lib/moxml.rb +1 -0
  24. data/spec/consistency/adapter_parity_spec.rb +2 -0
  25. data/spec/consistency/differential_taxonomy_spec.rb +117 -0
  26. data/spec/consistency/divergence_register_spec.rb +9 -1
  27. data/spec/consistency/eol_normalization_spec.rb +2 -0
  28. data/spec/consistency/loud_degradation_spec.rb +2 -0
  29. data/spec/consistency/metamorphic_properties_spec.rb +195 -0
  30. data/spec/consistency/metamorphic_spec.rb +138 -0
  31. data/spec/consistency/model_coverage_audit_spec.rb +108 -0
  32. data/spec/consistency/reference_differential_spec.rb +186 -0
  33. data/spec/consistency/xpath_idioms_spec.rb +11 -0
  34. data/spec/consistency/xslt_spec.rb +2 -0
  35. data/spec/moxml/adapter/leptris_attribute_snapshot_spec.rb +10 -7
  36. data/spec/moxml/adapter/leptris_contract_reads_spec.rb +29 -0
  37. data/spec/moxml/adapter/leptris_detached_text_spec.rb +10 -3
  38. data/spec/moxml/adapter/leptris_parse_lint_spec.rb +12 -0
  39. data/spec/moxml/adapter/ox_spec.rb +1 -1
  40. data/spec/moxml/attribute_resolver_spec.rb +2 -0
  41. data/spec/moxml/c14n/adapter_delegation_spec.rb +2 -0
  42. data/spec/moxml/doctype_spec.rb +7 -0
  43. data/spec/moxml/document_parse_diagnostics_spec.rb +2 -0
  44. data/spec/moxml/entity_spec.rb +2 -0
  45. data/spec/moxml/plan_conformance_spec.rb +2 -0
  46. data/spec/moxml/processing_instruction_spec.rb +9 -0
  47. data/spec/moxml/reference_opal_spec.rb +29 -0
  48. data/spec/moxml/reference_spec.rb +162 -0
  49. data/spec/moxml/sax_spec.rb +2 -0
  50. data/spec/moxml/text_spec.rb +2 -0
  51. data/spec/moxml/to_html_spec.rb +2 -0
  52. data/spec/moxml/xhtml_output_spec.rb +2 -0
  53. data/spec/moxml/xpath/axes_spec.rb +2 -2
  54. data/spec/moxml/xsd_spec.rb +38 -0
  55. data/spec/support/input_generator.rb +154 -0
  56. data/spec/support/input_taxonomy.rb +212 -0
  57. data/spec/support/xml_generator.rb +54 -0
  58. metadata +17 -2
@@ -0,0 +1,205 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ # Executable reference semantics (TODO.sota/03): a pure-Ruby XML
5
+ # reader, writer, and normalizer that answer the processing model
6
+ # without any native engine. Adapters are differentially tested
7
+ # against these answers; the reference is also the lane for
8
+ # engine-free environments (the Opal line generalized).
9
+ #
10
+ # The reference is deliberately small and total over the model's
11
+ # declared input taxonomy — every well-formed document the model
12
+ # admits is parseable here. It is an oracle, not a production
13
+ # parser: no recovery, no diagnostics beyond a named ParseError.
14
+ module Reference
15
+ autoload :Parser, "moxml/reference/parser"
16
+ autoload :Writer, "moxml/reference/writer"
17
+ autoload :Normalizer, "moxml/reference/normalizer"
18
+
19
+ # A reference node: the information set of the processing model,
20
+ # with no engine-specific shape.
21
+ class Node
22
+ attr_accessor :kind, :name, :attributes, :children, :data, :parent
23
+
24
+ def initialize(kind:, name: nil, data: nil, attributes: [])
25
+ @kind = kind
26
+ @name = name
27
+ @data = data
28
+ @attributes = attributes
29
+ @children = []
30
+ @parent = nil
31
+ end
32
+
33
+ def element?
34
+ @kind == :element
35
+ end
36
+
37
+ def text?
38
+ @kind == :text
39
+ end
40
+
41
+ # The facade's name face: a bare local name for prefixed sources
42
+ # (Element#name; attribute pairs list bare names too).
43
+ def local_name
44
+ return @name unless @name&.include?(":")
45
+
46
+ @name.split(":", 2)[1]
47
+ end
48
+
49
+ # Concatenated descendant text (elements; text and CDATA only —
50
+ # comments and PI data are not text); own data for text-like
51
+ # nodes (the model's `text` face).
52
+ def text
53
+ case @kind
54
+ when :element
55
+ @children.select do |child|
56
+ child.kind == :text || child.kind == :cdata || child.element?
57
+ end.map(&:text).join
58
+ when :text, :cdata
59
+ @data
60
+ else
61
+ ""
62
+ end
63
+ end
64
+
65
+ # Element content is its text face; text-like nodes answer their
66
+ # data (PI data excludes the separator, per the model).
67
+ def content
68
+ element? ? text : @data
69
+ end
70
+
71
+ def attribute(name)
72
+ pair = @attributes.find do |(key, _)|
73
+ key == name || local_name_of(key) == name
74
+ end
75
+ pair && pair[1]
76
+ end
77
+
78
+ # The facade's listing face: bare names, source order, duplicates
79
+ # included, namespace declarations excluded (they are Namespace
80
+ # nodes, not attributes — uniform with every adapter).
81
+ def attribute_pairs
82
+ @attributes.filter_map do |(key, value)|
83
+ next if key == "xmlns" || key.start_with?("xmlns:")
84
+
85
+ [local_name_of(key), value]
86
+ end
87
+ end
88
+
89
+ def local_name_of(name)
90
+ return name unless name&.include?(":")
91
+
92
+ name.split(":", 2)[1]
93
+ end
94
+ end
95
+
96
+ # A reference document: declaration and doctype are state (the
97
+ # model's document-parts-are-state rule); children are the
98
+ # top-level nodes excluding them.
99
+ class Document
100
+ attr_accessor :children, :declaration, :doctype, :entities
101
+
102
+ def initialize
103
+ @children = []
104
+ @declaration = nil
105
+ @doctype = nil
106
+ @entities = {}
107
+ end
108
+
109
+ def root
110
+ @children.find(&:element?)
111
+ end
112
+
113
+ # Depth-first pre-order, excluding the document itself.
114
+ def each_node(&block)
115
+ @children.each { |child| walk(child, &block) }
116
+ end
117
+
118
+ def walk(node, &block)
119
+ yield node
120
+ node.children.each { |child| walk(child, &block) }
121
+ end
122
+ end
123
+
124
+ # Encodings the reference accepts: UTF-8 and US-ASCII sources.
125
+ # Transcoding is the adapter's business; the reference reads the
126
+ # UTF-8 string the facade hands it.
127
+ module_function
128
+
129
+ # Parse an XML string into a reference Document.
130
+ def parse(xml)
131
+ Parser.new(xml).parse
132
+ end
133
+
134
+ # Document-order element names (the ordering face; bare local
135
+ # names, matching Element#name).
136
+ def element_names(document)
137
+ names = []
138
+ document.each_node { |node| names << node.local_name if node.element? }
139
+ names
140
+ end
141
+
142
+ # Element children names, keyed by each element's index in
143
+ # pre-order (the children-listing face; avoids text segmentation,
144
+ # which is a registered engine divergence family).
145
+ def child_element_names(document)
146
+ rows = []
147
+ document.each_node do |node|
148
+ next unless node.element?
149
+
150
+ rows << node.children.select(&:element?).map(&:local_name)
151
+ end
152
+ rows
153
+ end
154
+
155
+ # The access truth table, one row per element in pre-order:
156
+ # [name, text, content].
157
+ def element_access_rows(document)
158
+ rows = []
159
+ document.each_node do |node|
160
+ next unless node.element?
161
+
162
+ rows << [node.local_name, node.text, node.content]
163
+ end
164
+ rows
165
+ end
166
+
167
+ # PI targets and data, in document order.
168
+ def processing_instructions(document)
169
+ rows = []
170
+ document.each_node do |node|
171
+ rows << [node.name, node.data] if node.kind == :processing_instruction
172
+ end
173
+ rows
174
+ end
175
+
176
+ # Comment data, in document order.
177
+ def comments(document)
178
+ rows = []
179
+ document.each_node { |node| rows << node.data if node.kind == :comment }
180
+ rows
181
+ end
182
+
183
+ # Serialize the reference document as XML (indent: 0 canonical
184
+ # facade shape: declaration if the source had one, escaped text,
185
+ # self-closing empty elements).
186
+ def to_xml(document)
187
+ Writer.new(document).to_xml
188
+ end
189
+
190
+ # Serialize as HTML (void elements unclosed, raw script/style).
191
+ def to_html(document)
192
+ Writer.new(document).to_html
193
+ end
194
+
195
+ # Serialize as XHTML (void elements self-closed).
196
+ def to_xhtml(document)
197
+ Writer.new(document).to_xhtml
198
+ end
199
+
200
+ # EOL normalization per XML 1.0 §2.11.
201
+ def normalize_eol(text)
202
+ Normalizer.normalize_eol(text)
203
+ end
204
+ end
205
+ end
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.147"
4
+ VERSION = "0.5.149"
5
5
  end
@@ -30,9 +30,12 @@ module Moxml
30
30
  # Wildcard for node names/namespace prefixes
31
31
  STAR = "*"
32
32
 
33
- # Node types that require a NodeSet to push nodes into
33
+ # Node types that require a NodeSet to push nodes into.
34
+ # :union belongs here: a top-level union built its own NodeSet
35
+ # in on_pipe and bypassed the document-order wrapper — union
36
+ # results answered in expression order (M3).
34
37
  RETURN_NODESET = %i[path absolute_path relative_path axis
35
- predicate].freeze
38
+ predicate union].freeze
36
39
 
37
40
  # Compiles and caches an AST
38
41
  #
@@ -94,8 +97,14 @@ module Moxml
94
97
  [nodeset_class, "new", empty_array,
95
98
  context_var])
96
99
 
100
+ # Multi-step paths batch step N over every step-(N-1)
101
+ # node, concatenating subtree results after sibling
102
+ # results (//* answered a,c,b on <r><a><b/></a><c/></r>);
103
+ # XPath path results are conventionally document-ordered.
104
+ ordered = Ruby::Node.new(:send, [matched, "document_order"])
97
105
  body = matched.assign(nodeset_new)
98
106
  .followed_by(ruby_ast)
107
+ .followed_by(matched.assign(ordered))
99
108
  .followed_by(matched)
100
109
  else
101
110
  body = ruby_ast
@@ -293,6 +302,12 @@ module Moxml
293
302
  end
294
303
  end
295
304
  end
305
+
306
+ # Multi-step evaluation batches step N over every step-(N-1)
307
+ # node, so subtree results concatenate after sibling
308
+ # results (//* on <r><a><b/></a><c/></r> answered a,c,b).
309
+ # Path results are conventionally document-ordered: re-sort
310
+ # the union when the caller takes the node-set form.
296
311
  end
297
312
  end
298
313
 
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ module XPath
5
+ # Document ordering for node-set results (TODO.sota/02, M3).
6
+ #
7
+ # XPath defines node-sets as unordered, but path-expression
8
+ # results are conventionally in document order (and consumers
9
+ # rely on it). Multi-step paths are evaluated by batching step N
10
+ # over every step-(N-1) node, which concatenates subtree results
11
+ # after sibling results — results must therefore be re-ordered.
12
+ #
13
+ # Ordering is a ONE-PASS position index per document, keyed by
14
+ # native identity: `traverse` yields nodes in document order, and
15
+ # the engines that re-order here (oga; the pure-Ruby XPath engine
16
+ # lanes) expose stable native objects per node. The ancestry-walk
17
+ # key (`key_for`) is quadratic on wide documents — every node
18
+ # compares its way through its siblings' structural equality —
19
+ # and is kept only as a fallback for natives the index does not
20
+ # know (attribute results, unstable identities).
21
+ module Ordering
22
+ module_function
23
+
24
+ INDEXED_ELEMENT_RANK = 1
25
+ ATTRIBUTE_RANK = 0
26
+ FALLBACK_RANK = 2
27
+
28
+ # Below this result size the ancestry walk is cheaper than one
29
+ # document traverse (the index only pays off on wide results).
30
+ INDEX_THRESHOLD = 16
31
+
32
+ # Sorts wrappers into document order. Attributes sort before
33
+ # the children of their element (XPath attribute-then-child
34
+ # ordering). Nodes the index cannot place keep their relative
35
+ # position at the end (stable sort).
36
+ def document_order(nodes)
37
+ return nodes if nodes.size < 2
38
+
39
+ indexed = if nodes.size < INDEX_THRESHOLD
40
+ nodes.each_with_index.map do |node, i|
41
+ [fallback_key(node), i, node]
42
+ end
43
+ else
44
+ positions = position_index(nodes.first)
45
+ nodes.each_with_index.map do |node, i|
46
+ [positions.fetch(node.native) { fallback_key(node) },
47
+ i, node]
48
+ end
49
+ end
50
+ indexed.sort_by { |key, i, _| [key, i] }.map(&:last)
51
+ end
52
+
53
+ # native => [rank, position] for every node in document order.
54
+ # compare_by_identity: the natives are the engine's own objects,
55
+ # and native equality may be structural (Oga compares subtrees).
56
+ def position_index(node)
57
+ doc = node
58
+ doc = doc.parent while doc && !doc.is_a?(Moxml::Document)
59
+ return {}.compare_by_identity unless doc
60
+
61
+ index = {}.compare_by_identity
62
+ position = 0
63
+ doc.traverse do |n|
64
+ index[n.native] = [INDEXED_ELEMENT_RANK, position]
65
+ position += 1
66
+ end
67
+ index
68
+ end
69
+
70
+ # Documents with unstable native wrappers (a fresh object per
71
+ # access) fall back to the ancestry path: correct, and only the
72
+ # answer for lanes whose engine already orders natively.
73
+ def fallback_key(node)
74
+ kind_rank = node.is_a?(Moxml::Attribute) ? ATTRIBUTE_RANK : FALLBACK_RANK
75
+ [kind_rank, ancestry_path(node)]
76
+ end
77
+
78
+ def ancestry_path(node)
79
+ path = []
80
+ current = node
81
+ while current && !current.is_a?(Moxml::Document)
82
+ parent = current.parent
83
+ break unless parent
84
+
85
+ siblings = parent.children.to_a
86
+ idx = siblings.index { |s| s.equal?(current) } ||
87
+ siblings.index(current)
88
+ return [2_000_000] if idx.nil?
89
+
90
+ path.unshift(idx)
91
+ current = parent
92
+ end
93
+ path
94
+ end
95
+ end
96
+ end
97
+ end
data/lib/moxml/xpath.rb CHANGED
@@ -9,6 +9,7 @@ module Moxml
9
9
  autoload :Lexer, "moxml/xpath/lexer"
10
10
  autoload :Parser, "moxml/xpath/parser"
11
11
  autoload :Compiler, "moxml/xpath/compiler"
12
+ autoload :Ordering, "moxml/xpath/ordering"
12
13
 
13
14
  autoload :Error, "moxml/xpath/errors"
14
15
  autoload :SyntaxError, "moxml/xpath/errors"
data/lib/moxml.rb CHANGED
@@ -102,6 +102,7 @@ module Moxml
102
102
  autoload :SAX, "moxml/sax"
103
103
  autoload :Signature, "moxml/signature"
104
104
  autoload :C14n, "moxml/c14n"
105
+ autoload :Reference, "moxml/reference"
105
106
  autoload :XSLT, "moxml/xslt"
106
107
  autoload :XSD, "moxml/xsd"
107
108
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Processing model §Document order and traversal (normative): pinned here.
4
+
3
5
  RSpec.describe "Adapter Examples" do
4
6
  let(:xml) { "<root><child>text</child></root>" }
5
7
 
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "support/input_taxonomy"
5
+
6
+ # Whole-class differential (TODO.sota/01): for every taxonomy input,
7
+ # every available adapter answers the same observable results — or the
8
+ # difference cites a registered divergence (D-NN). Faces: node count,
9
+ # root name, text, element count via XPath. Plus M1 (parse∘serialize
10
+ # round-trip) per adapter over every well-formed input. Nothing is
11
+ # exempt without a D-ID: this is the generated-evidence face of the
12
+ # conformance relation.
13
+
14
+ RSpec.describe "cross-adapter taxonomy differential" do
15
+ def snapshot(doc)
16
+ {
17
+ nodes: doc.traverse.to_a.grep_v(Moxml::Document).count,
18
+ root: doc.root&.name,
19
+ text: doc.text,
20
+ xpath: doc.xpath("//*").to_a.count,
21
+ }
22
+ end
23
+
24
+ def answers(xml)
25
+ Moxml::Adapter::AVAILABLE_ADAPTERS.filter_map do |name|
26
+ doc = Moxml.new(name).parse(xml)
27
+ [name, snapshot(doc)]
28
+ rescue Moxml::Error, NotImplementedError, LoadError
29
+ nil # adapter unavailable in this environment
30
+ end.to_h
31
+ end
32
+
33
+ InputTaxonomy.well_formed.each do |input|
34
+ it "answers #{input.id} identically across adapters" do
35
+ per = answers(input.xml)
36
+ expect(per.keys.count).to be >= 2
37
+
38
+ base_name = per.keys.first
39
+ base = per[base_name]
40
+ per.each do |name, got|
41
+ base.except(:text).each_key do |face|
42
+ exempt = InputTaxonomy::EXEMPT.dig(input.id, face, name)
43
+ next if exempt
44
+
45
+ expect(got[face]).to eq(base[face]),
46
+ "#{input.id}/#{face} differs on #{name} vs " \
47
+ "#{base_name}: #{got[face].inspect} vs " \
48
+ "#{base[face].inspect} — fix it or register " \
49
+ "a divergence and exempt the cell"
50
+ end
51
+ text_exempt = InputTaxonomy::EXEMPT.dig(input.id, :text, name)
52
+ next if text_exempt
53
+
54
+ expect(got[:text]).to eq(base[:text]),
55
+ "#{input.id}/text differs on #{name} vs " \
56
+ "#{base_name}: #{got[:text].inspect} vs " \
57
+ "#{base[:text].inspect} — fix it or register " \
58
+ "a divergence and exempt the cell"
59
+ end
60
+ end
61
+ end
62
+
63
+ it "keeps the round trip stable per adapter (M1: parse∘serialize)" do # rubocop:disable RSpec/NoExpectationExample
64
+ InputTaxonomy.well_formed.each do |input|
65
+ Moxml::Adapter::AVAILABLE_ADAPTERS.each do |name|
66
+ begin
67
+ ctx = Moxml.new(name)
68
+ doc = ctx.parse(input.xml)
69
+ first = snapshot(doc)
70
+ # indent: 0 — default indentation synthesizes whitespace text
71
+ # nodes on re-parse (formatting, not information; D-09
72
+ # adapters ignore the knob and may embed their own pretty
73
+ # newlines regardless). M1 therefore compares the information
74
+ # set: root, element count, whitespace-collapsed text. Node
75
+ # COUNT identity is the cross-adapter face's job (same parse),
76
+ # not the round-trip's.
77
+ second = snapshot(ctx.parse(doc.to_xml(indent: 0)))
78
+ rescue Moxml::Error, NotImplementedError, LoadError
79
+ # Named guards (e.g. D-16's read cap on rexml) legitimately
80
+ # excuse an adapter from M1 for an input.
81
+ next
82
+ end
83
+ compact = ->(snap) {
84
+ snap.except(:nodes).merge(text: snap[:text].gsub(/\s+/, ""))
85
+ }
86
+ expect(compact.call(second)).to eq(compact.call(first)),
87
+ "#{input.id} round-trip drifted on " \
88
+ "#{name}: #{first.inspect} vs " \
89
+ "#{second.inspect}"
90
+ end
91
+ end
92
+ end
93
+
94
+ it "raises the named parse error on strict malformed input, everywhere" do
95
+ # Entity/reference and namespace-validation classes are
96
+ # engine-variant by design — moxml governs them with config
97
+ # (entity modes, namespace validation), not with a registered
98
+ # divergence.
99
+ engine_variant = %w[undefined reference undeclared prefix].freeze
100
+ inputs = InputTaxonomy.malformed.reject do |input|
101
+ engine_variant.any? { |term| input.id.include?(term) }
102
+ end
103
+
104
+ inputs.each do |input|
105
+ Moxml::Adapter::AVAILABLE_ADAPTERS.each do |name|
106
+ next if InputTaxonomy::STRICT_EXEMPT.dig(input.id, name)
107
+
108
+ # The strict lane, named: the model's default posture for
109
+ # malformed input is a named raise on every adapter.
110
+ expect { Moxml.new(name).parse(input.xml, strict: true) }
111
+ .to raise_error(Moxml::ParseError),
112
+ "#{input.id}/#{name}: strict parse accepted malformed " \
113
+ "input or raised outside the named ParseError contract"
114
+ end
115
+ end
116
+ end
117
+ end
@@ -9,7 +9,7 @@ require "spec_helper"
9
9
  # no spec cites an unregistered ID.
10
10
  RSpec.describe "divergence register linkage" do
11
11
  REGISTER_PATH = "docs/_pages/divergences.adoc" # rubocop:disable Lint/ConstantDefinitionInBlock, RSpec/LeakyConstantDeclaration
12
- SCAN_GLOBS = %w[spec/consistency/**/*.rb spec/moxml/**/*.rb].freeze # rubocop:disable Lint/ConstantDefinitionInBlock, RSpec/LeakyConstantDeclaration
12
+ SCAN_GLOBS = %w[spec/consistency/**/*.rb spec/moxml/**/*.rb spec/support/*.rb].freeze # rubocop:disable Lint/ConstantDefinitionInBlock, RSpec/LeakyConstantDeclaration
13
13
  NEXT_GATE = [ # rubocop:disable Lint/ConstantDefinitionInBlock, RSpec/LeakyConstantDeclaration
14
14
  /same engine as/, # engine dedup, not a divergence
15
15
  /source_adapter == target_adapter/, # self round-trip
@@ -18,7 +18,15 @@ RSpec.describe "divergence register linkage" do
18
18
  /skip_shapes/, # shape routing (IDs cited above)
19
19
  /leptris_fixed\?/, # version gate
20
20
  /source_elements/, # data-presence guard
21
+ /next unless doc/, # adapter-availability guard (not a
22
+ # divergence: the adapter is not installed in this environment)
23
+ /next unless cell\[:pin\]/, # matrix data-presence guard
21
24
  /next unless match/, # scanner-internal guard
25
+ /well_formed|voids\.empty\?|default_ns|per\.size/, # property-scope
26
+ # and availability guards (input scoping, not adapter exemptions)
27
+ /exempt/i, # value-routed exemption table: the
28
+ # table's VALUES are D-IDs, enforced by
29
+ # the coverage audit's exempt rule
22
30
  ].freeze
23
31
 
24
32
  # Version/availability gates are not divergences: a binding too old
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Processing model §Parsing (normative): pinned here.
4
+
3
5
  require "spec_helper"
4
6
 
5
7
  # XML 1.0 §2.11 End-of-Line Handling: literal CRLF and lone CR
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Processing model §Errors and loud degradation (normative): pinned here.
4
+
3
5
  require "spec_helper"
4
6
 
5
7
  # Loud degradation (processing model, "Errors and loud degradation"):