canon 0.3.111 → 0.3.113

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: 88e8f69b4949d642507ea1039ab45a14273b42a806fbf2198444a0dd3ded62ae
4
- data.tar.gz: be39473082b9bdf3b32174a6bd217face312dd0e2872fec990ca5f6bf3d54c7e
3
+ metadata.gz: c91f395c09165032ecc68e5a79f01c0620a66571411add6b4e083d68b9367d10
4
+ data.tar.gz: 1bbabc0e6112d88e3743fb20f0cb22f765b1a6d2fa8fd655f31b1bb2b368ce71
5
5
  SHA512:
6
- metadata.gz: 68c47754c52898a62221e489896ece0ca5dc2560d3821683b4404d823fb80d26f4513be7475ecbf2b2846c26e27a84b0d1d2aa3420ffdde2b6071d5dc5e31e6a
7
- data.tar.gz: 5b4b67fa6dbeb20764fc40c1d747597b41c6829e575506bbc915fb567036b0b2c33969f3ff01f7ccb414785aa79bb078f7273bec09f29cf58ff2ceec039063cc
6
+ metadata.gz: a94fd8fc2dc5493976494b2a0a3ab9031537d93557005c1a7c51405d4b3a64ab5a07806ac43831a41bddc194db61b231576481e151d994001287ef215c9165d8
7
+ data.tar.gz: f83ba6f4dc985daa93f062f4bffecd2adaf5a47c77473106263f810112e54e65a6da39163d47c06eb057832abebb7a53f6c0e31ec98ed9b445d1de31574e3ddc
@@ -0,0 +1,289 @@
1
+ = Canon comparison theory: dimensions, profiles, and the two algorithms
2
+ :toc:
3
+
4
+ This document is the specification of canon's comparison semantics.
5
+ It is REVIEWED AGAINST THE IMPLEMENTATION by
6
+ `spec/canon/docs/comparison_theory_spec.rb`: every dimension,
7
+ behavior, and profile row cited here must exist in the dimension
8
+ registry and the profile resolvers, and everything the registry
9
+ defines must appear here (docs match shipped behavior — the W6.3
10
+ standing rule).
11
+
12
+ == 1. The four-layer pipeline
13
+
14
+ Comparison flows through four independent layers. Each layer's
15
+ output is the next layer's input; no layer sees another layer's
16
+ options.
17
+
18
+ [cols="1,3,2"]
19
+ |===
20
+ |Layer |Decides |Resolved from
21
+
22
+ |1. Preprocessing
23
+ |What byte form enters the parse (`:none`, `:c14n`, `:normalize`,
24
+ `:format`, `:rendered`)
25
+ |The profile's `preprocessing` key
26
+
27
+ |2. Algorithm
28
+ |The comparison engine: `:dom` (position-based recursive descent) or
29
+ `:semantic` (signature matching producing insert/update/delete/move)
30
+ |`diff_algorithm:` (default `:dom`); the two are distinct by design
31
+ and are never merged
32
+
33
+ |3. Matching
34
+ |How nodes are deemed equal: one behavior per dimension
35
+ |The profile (or explicit per-dimension options) resolved through
36
+ the format's resolver
37
+
38
+ |4. Presentation
39
+ |How results are spelled (`by_line`, `by_object`, normative-only
40
+ filtering)
41
+ |`show_diffs:` and the formatter choice
42
+ |===
43
+
44
+ == 2. Dimensions
45
+
46
+ A *dimension* is one axis along which two nodes may differ. The
47
+ dimension registry (`Canon::Comparison::Dimensions::Registry`)
48
+ defines, per format, each dimension's name and its *valid
49
+ behaviors* — closed sets; an option outside the set raises at
50
+ resolution. HTML aliases (`html`, `html4`, `html5`) resolve to the
51
+ XML dimension set.
52
+
53
+ === 2.1 XML and HTML (the `:xml` set)
54
+
55
+ [cols="1,2,3"]
56
+ |===
57
+ |Dimension |Behaviors |Semantics
58
+
59
+ |`text_content`
60
+ |`strict`, `normalize`, `ignore`
61
+ |`strict`: character-exact text comparison. `normalize`:
62
+ whitespace runs collapse to a single space and leading/trailing
63
+ whitespace strips before comparison. `ignore`: text content never
64
+ differs.
65
+
66
+ |`structural_whitespace`
67
+ |`strict`, `normalize`, `ignore`
68
+ |Whitespace-only text nodes between elements. `strict`: they are
69
+ content (normative: a `strict`-only dimension — a non-strict
70
+ behavior can never make documents inequivalent on its own).
71
+ `normalize`: whitespace-only nodes are equivalent regardless of
72
+ shape. `ignore`: whitespace-only nodes are dropped before the walk.
73
+
74
+ |`attribute_presence`
75
+ |`strict`, `ignore`
76
+ |Whether an attribute exists. `ignore` drops all attributes before
77
+ comparison.
78
+
79
+ |`attribute_order`
80
+ |`strict`, `ignore`
81
+ |`strict`: attribute order is significant. `ignore`: attributes
82
+ compare as a set.
83
+
84
+ |`attribute_values`
85
+ |`strict`, `strip`, `compact`, `normalize`, `ignore`
86
+ |`strict`: character-exact values. `strip`: leading/trailing
87
+ whitespace strips. `compact`: internal whitespace runs collapse
88
+ (verify: the value is compared after whitespace compaction).
89
+ `normalize`: both strip and collapse. `ignore`: values compare
90
+ equal regardless.
91
+
92
+ |`element_position`
93
+ |`strict`, `ignore`
94
+ |`strict`: child order is significant. `ignore`: children match by
95
+ identity (the ElementMatcher's id-attribute matching), not position.
96
+
97
+ |`comments`
98
+ |`strict`, `ignore`
99
+ |Comment nodes: content-compared or dropped.
100
+
101
+ |`namespace_prefix`
102
+ |`significant`, `ignore`
103
+ |`significant`: the written prefix is part of the node's identity.
104
+ `ignore`: only the namespace URI matters; the prefix is spelling.
105
+ |===
106
+
107
+ === 2.2 JSON (the `:json` set)
108
+
109
+ [cols="1,2,3"]
110
+ |===
111
+ |Dimension |Behaviors |Semantics
112
+
113
+ |`text_content`
114
+ |`strict`, `normalize`, `ignore`
115
+ |String values, `strict` character-exact.
116
+
117
+ |`structural_whitespace`
118
+ |`strict`, `normalize`, `ignore`
119
+ |Whitespace between tokens outside strings. `strict`-only
120
+ normative.
121
+
122
+ |`key_order`
123
+ |`strict`, `ignore`
124
+ |`ignore`: objects compare as unordered maps (the canonical lane
125
+ sorts keys).
126
+ |===
127
+
128
+ === 2.3 YAML (the `:yaml` set)
129
+
130
+ The `:json` set plus:
131
+
132
+ [cols="1,2,3"]
133
+ |===
134
+ |Dimension |Behaviors |Semantics
135
+
136
+ |`comments`
137
+ |`strict`, `ignore`
138
+ |YAML comments: compared or dropped before parse.
139
+ |===
140
+
141
+ == 3. Profiles and the profile algebra
142
+
143
+ A *profile* is a named total assignment: one behavior per dimension
144
+ of the format, plus a preprocessing layer. Profiles resolve through
145
+ the format's resolver; explicit per-dimension options override a
146
+ profile's entry for that dimension alone. XML/HTML ship FOUR
147
+ profiles; JSON and YAML ship THREE (`:rendered` does not exist for
148
+ them — the resolvers reject it). The resolved assignments below are
149
+ asserted by the review spec:
150
+
151
+ [cols="1,1,1"]
152
+ |===
153
+ |Profile |Intent |Resolved assignment
154
+
155
+ |`:strict`
156
+ |Default. Everything significant.
157
+ |`text_content=strict`, `structural_whitespace=strict`,
158
+ `attribute_presence=strict`, `attribute_order=strict`,
159
+ `attribute_values=strict`, `element_position=strict`,
160
+ `comments=strict`, `namespace_prefix=significant`,
161
+ `whitespace_type=strict`, `preprocessing=none`.
162
+
163
+ |`:spec_friendly`
164
+ |Normative-document comparison (the spec-familiar shape).
165
+ |`text_content=normalize`, `structural_whitespace=ignore`,
166
+ `attribute_presence=strict`, `attribute_values=normalize`,
167
+ `attribute_order=ignore`, `element_position=ignore`,
168
+ `comments=ignore`, `namespace_prefix=ignore`,
169
+ `whitespace_type=strict`, `preprocessing=rendered`.
170
+
171
+ |`:rendered` (XML/HTML only)
172
+ |The same document as rendered sources.
173
+ |`text_content=normalize`, `structural_whitespace=normalize`,
174
+ `attribute_presence=strict`, `attribute_order=ignore`,
175
+ `comments=ignore`, `attribute_values=strict`,
176
+ `element_position=strict`, `namespace_prefix=significant`,
177
+ `whitespace_type=strict`, `preprocessing=none`.
178
+
179
+ |`:content_only`
180
+ |Ignore all spelling; compare information content.
181
+ |`text_content=normalize`, `structural_whitespace=ignore`,
182
+ `attribute_presence=strict`, `attribute_order=ignore`,
183
+ `attribute_values=normalize`, `element_position=ignore`,
184
+ `comments=ignore`, `namespace_prefix=ignore`,
185
+ `whitespace_type=strict`, `preprocessing=c14n`.
186
+ |===
187
+
188
+ === 3.1 JSON and YAML profile assignments
189
+
190
+ JSON ships `:strict`, `:spec_friendly`, and `:content_only` (no
191
+ `:rendered`); YAML the same plus its `comments` dimension.
192
+
193
+ [cols="1,1,1"]
194
+ |===
195
+ |Profile |JSON |YAML
196
+
197
+ |`:strict`
198
+ |`key_order=strict`, `text_content=strict`,
199
+ `structural_whitespace=strict`, `preprocessing=none`
200
+ |`key_order=strict`, `text_content=strict`,
201
+ `structural_whitespace=strict`, `comments=strict`,
202
+ `preprocessing=none`
203
+
204
+ |`:spec_friendly`
205
+ |`key_order=ignore`, `text_content=strict`,
206
+ `structural_whitespace=ignore`, `preprocessing=normalize`
207
+ |`key_order=ignore`, `text_content=strict`,
208
+ `structural_whitespace=ignore`, `comments=ignore`,
209
+ `preprocessing=normalize`
210
+
211
+ |`:content_only`
212
+ |`key_order=ignore`, `text_content=normalize`,
213
+ `structural_whitespace=ignore`, `preprocessing=normalize`
214
+ |`key_order=ignore`, `text_content=normalize`,
215
+ `structural_whitespace=ignore`, `comments=ignore`,
216
+ `preprocessing=normalize`
217
+ |===
218
+
219
+ *Registry note (the W6.2 audit finding)*: the resolvers resolve
220
+ one key beyond the registry's dimension sets — `whitespace_type`
221
+ (`strict` in every shipped profile) — governing whether whitespace
222
+ TYPE differences (space vs tab) are significant. It awaits registry
223
+ promotion; until then it is documented here as a resolver-level
224
+ option.
225
+
226
+ Profile algebra properties:
227
+
228
+ * *Totality*: a resolved profile assigns every format dimension
229
+ exactly one behavior (the resolver raises on a gap).
230
+ * *Monotone relaxation*: `:strict` is the identity of the algebra —
231
+ every other profile differs from `:strict` only by relaxing
232
+ dimensions toward `ignore`/`normalize`, never by strengthening.
233
+ * *Non-normativity of relaxed dimensions*: with the exception of
234
+ `text_content`, a dimension at `ignore` contributes no
235
+ equivalence-affecting differences (the normative/informative split
236
+ below).
237
+
238
+ == 4. Normative, informative, and formatting-only differences
239
+
240
+ A difference is classified when emitted:
241
+
242
+ * *Normative* — affects equivalence: the documents are not
243
+ equivalent. Produced by a dimension at a strict/significant
244
+ behavior.
245
+ * *Informative* — recorded but not equivalence-affecting: produced
246
+ by a dimension at a relaxed behavior where the difference is still
247
+ observable.
248
+ * *Formatting-only* — the canonical forms match; the difference is
249
+ pure spelling.
250
+
251
+ `show_diffs: :normative` filters the presentation to normative
252
+ findings. The classification is a property of the dimension's
253
+ resolved behavior, not of the documents.
254
+
255
+ == 5. The two algorithms
256
+
257
+ === 5.1 DOM (`diff_algorithm: :dom`, default)
258
+
259
+ Position-based recursive descent. For each element pair: compare
260
+ names, dimensions resolve per-dimension (attributes as sets or
261
+ sequences per `attribute_order`, children positionally per
262
+ `element_position` with the ElementMatcher's identity matching when
263
+ relaxed). Complexity is linear in the sum of tree sizes. The
264
+ intermediate representation is comparison-result codes
265
+ (`EQUIVALENT`, `MISSING_NODE`, `UNEQUAL_TEXT_CONTENTS`, ...) which
266
+ convert to DiffNodes.
267
+
268
+ === 5.2 Semantic (`diff_algorithm: :semantic`)
269
+
270
+ Signature-based tree matching in three phases: (1) hash matching —
271
+ identical subtrees match exactly; (2) similarity matching —
272
+ remaining nodes pair by content similarity above
273
+ `similarity_threshold`; (3) structural propagation — parent/child
274
+ correspondences derive from matched ancestors. The intermediate
275
+ representation is an operation stream (INSERT, DELETE, UPDATE,
276
+ MOVE) converted to DiffNodes. The algorithms share preprocessing,
277
+ configuration, match options, and output formatting; the comparison
278
+ cores never share code (distinct by design — see CLAUDE.md, "Two
279
+ Comparison Algorithms — Distinct by Design").
280
+
281
+ == 6. Registry citation
282
+
283
+ The review spec (`spec/canon/docs/comparison_theory_spec.rb`)
284
+ asserts, at every run: for each format, every registry dimension
285
+ name and every valid behavior token appears in this document; every
286
+ profile row's resolved assignment equals the resolver's output for
287
+ that profile. A dimension added to the registry without a §2 entry,
288
+ a behavior added without its semantics row, or a profile change
289
+ without a §3 row update, fails the battery.
data/lib/canon/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Canon
4
- VERSION = "0.3.111"
4
+ VERSION = "0.3.113"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: canon
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.111
4
+ version: 0.3.113
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -173,6 +173,7 @@ files:
173
173
  - docs/Gemfile
174
174
  - docs/INDEX.adoc
175
175
  - docs/_config.yml
176
+ - docs/_references/comparison-theory.adoc
176
177
  - docs/advanced/diff-classification.adoc
177
178
  - docs/advanced/diff-pipeline.adoc
178
179
  - docs/advanced/extending-canon.adoc