canon 0.3.115 → 0.3.117
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 +4 -4
- data/docs/_references/canonical-digests.adoc +50 -0
- data/docs/_references/jcs-rfc8785.adoc +52 -0
- data/lib/canon/comparison/schema_gate.rb +29 -0
- data/lib/canon/comparison.rb +28 -0
- data/lib/canon/digest.rb +44 -0
- data/lib/canon/validators/xsd_validator.rb +57 -0
- data/lib/canon/validators.rb +1 -0
- data/lib/canon/version.rb +1 -1
- data/lib/canon/xml/exclusive_c14n.rb +24 -6
- data/lib/canon.rb +11 -0
- data/lib/tasks/performance_comparator.rb +19 -2
- metadata +7 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 223b5ab0201bbebfe8bfee1de2f23229260f574bd6d42fed426256b199f03d6c
|
|
4
|
+
data.tar.gz: 803eaba7a9fd850a38669c9ea2bbd567503fbc936fd39147a473d2526b3539ce
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 24ae7e292b598242df4c98bad7d98d98815d7d28ddebd6d1ed79df4451e40017cd3840f2a3556bf4b78681ee7b6653ee91a67623d806d2aec1d8e170093a19d8
|
|
7
|
+
data.tar.gz: 35a5642e0e6b5b0268935d6888470614058888a3e4cc33d587ea079737a3817b9543aa63fbae986da95240a2c4c72174419eae1fba9336e871859ae6aae6ee3d
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
= The canonical digests (W5.3)
|
|
2
|
+
|
|
3
|
+
This document specifies canon's canonical digest API as shipped by
|
|
4
|
+
`Canon::Digest`. Reviewed against the implementation by
|
|
5
|
+
`spec/canon/docs/canonical_digests_spec.rb` (the W6.3 review-gate
|
|
6
|
+
pattern), with the cross-release byte pins carried by
|
|
7
|
+
`spec/canon/digest_stability_spec.rb`.
|
|
8
|
+
|
|
9
|
+
== 1. The law
|
|
10
|
+
|
|
11
|
+
The digest of a document is the OpenSSL hash of its CANONICAL
|
|
12
|
+
FORM — the form under which the format's comparison pipeline deems
|
|
13
|
+
documents equivalent. Two documents that compare equivalent produce
|
|
14
|
+
identical digests; two that compare distinct produce distinct
|
|
15
|
+
digests. The digest is an API over the comparison semantics, not a
|
|
16
|
+
new canonicalization.
|
|
17
|
+
|
|
18
|
+
== 2. Per-format canonical forms
|
|
19
|
+
|
|
20
|
+
* *XML* rides Canonical XML 1.1
|
|
21
|
+
(`Canon::Xml::C14n.canonicalize`, reached as
|
|
22
|
+
`Formatters::XmlFormatter.format(pretty: false)`). The
|
|
23
|
+
pretty-printed XML form preserves attribute order and is NOT
|
|
24
|
+
canonical; the digest lane never touches it.
|
|
25
|
+
* *JSON and YAML* ride the key-sorted formatter forms (the JCS
|
|
26
|
+
profile for JSON's canonical lane; the `[class, to_s]` TOTAL
|
|
27
|
+
key order for YAML).
|
|
28
|
+
* *HTML* rides the whitespace-law form with sorted attributes
|
|
29
|
+
(the W1.3 specification).
|
|
30
|
+
|
|
31
|
+
Equivalence consequences the digest inherits: attribute order is
|
|
32
|
+
insignificant in XML and HTML; mapping key order is insignificant
|
|
33
|
+
in JSON and YAML; element/sequence order is significant everywhere.
|
|
34
|
+
|
|
35
|
+
== 3. Entry points
|
|
36
|
+
|
|
37
|
+
`Canon::Digest.digest(content, format, algorithm: "sha256")`
|
|
38
|
+
accepts any OpenSSL digest name; `canonical_form(content, format)`
|
|
39
|
+
exposes the hashed bytes. Per-format shorthands
|
|
40
|
+
(`Canon.digest_xml`, `digest_yaml`, `digest_json`, ...) mirror the
|
|
41
|
+
format lane's shorthands.
|
|
42
|
+
|
|
43
|
+
== 4. Stability contract
|
|
44
|
+
|
|
45
|
+
The digests are stable across releases for as long as the
|
|
46
|
+
underlying canonical form is. The stability suite pins the digest
|
|
47
|
+
hex of a representative document per format; a canonical-form
|
|
48
|
+
change moves a digest and MUST fail that suite, forcing a
|
|
49
|
+
conscious pin update (and a release note) rather than silent drift
|
|
50
|
+
for downstream dedup stores.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
= The JCS canonical profile (W6.1, RFC 8785)
|
|
2
|
+
|
|
3
|
+
This document specifies canon's JSON JCS canonical form as shipped
|
|
4
|
+
by `Canon::Json::JCS`. Reviewed against the implementation by
|
|
5
|
+
`spec/canon/docs/jcs_spec_review.rb` (the W6.3 review-gate
|
|
6
|
+
pattern).
|
|
7
|
+
|
|
8
|
+
== 1. Scope
|
|
9
|
+
|
|
10
|
+
RFC 8785 — the I-JSON Canonicalization Scheme. Input must be I-JSON
|
|
11
|
+
(RFC 7493): valid UTF-8 without lone surrogates, no duplicate
|
|
12
|
+
object keys, numbers expressible as IEEE 754 doubles. Violations
|
|
13
|
+
raise `Canon::Json::JCS::Error`.
|
|
14
|
+
|
|
15
|
+
== 2. The canonical laws
|
|
16
|
+
|
|
17
|
+
* *Strings* (`escape_string`): RFC 8785 §3.2.2.2 — the
|
|
18
|
+
JSON.stringify escaping set: short forms for `\b \t \n \f \r`
|
|
19
|
+
(the SHORT_ESCAPES table), two-hex lowercase `\u00xx` for the
|
|
20
|
+
remaining C0 controls, `\"` and `\\`; every other code point
|
|
21
|
+
emits verbatim as UTF-8 (DEL U+007F is NOT escaped).
|
|
22
|
+
|
|
23
|
+
* *Numbers* (`es6_number`): ECMAScript `Number::toString`
|
|
24
|
+
serialization — the shortest round-tripping decimal, fixed
|
|
25
|
+
notation inside [1e-6, 1e21), exponential outside; integers
|
|
26
|
+
via `ijson_integer` (the I-JSON 2^53 range).
|
|
27
|
+
|
|
28
|
+
* *Object keys* (`write_object`): sorted by their
|
|
29
|
+
UTF-16 code units (`utf16_code_units` — the RFC's sort is over
|
|
30
|
+
UTF-16 code units, not code points, so surrogate pairs and BMP
|
|
31
|
+
characters interleave per the RFC's examples; e.g. the key
|
|
32
|
+
U+1D160 (a surrogate pair, code units 0xD834 0xDD60) sorts
|
|
33
|
+
BEFORE U+20AC (a single BMP code unit 0x20AC), which sorts
|
|
34
|
+
BEFORE "a" (0x0061)).
|
|
35
|
+
|
|
36
|
+
* *Containers*: arrays comma-separated in order; objects
|
|
37
|
+
comma-separated in sorted key order; no whitespace anywhere.
|
|
38
|
+
|
|
39
|
+
== 3. Entry points
|
|
40
|
+
|
|
41
|
+
`canonicalize(json)` accepts a JSON string (parsed through
|
|
42
|
+
`JSON.parse`) or an already-parsed Ruby graph; `serialize(value)`
|
|
43
|
+
skips the parse. The canonical form is byte-stable across
|
|
44
|
+
platforms and suitable for hashing and signing.
|
|
45
|
+
|
|
46
|
+
== 4. Fixed point and parity
|
|
47
|
+
|
|
48
|
+
JCS output re-parses to the same graph, and the form is a fixed
|
|
49
|
+
point. The phase 02 property suite (JSON/YAML equivalence legs)
|
|
50
|
+
and the diff-display corpus pin the behavior; the JCS spec
|
|
51
|
+
vectors (RFC 8785 §3.2.3 examples) pin the number and sorting
|
|
52
|
+
laws.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Canon
|
|
4
|
+
module Comparison
|
|
5
|
+
# W5.1 validate-then-compare: runs both documents through the
|
|
6
|
+
# engine's XSD assessment before the diff. Returns a hash of
|
|
7
|
+
# side-index (0 or 1) to Canon::ValidationError — empty when both
|
|
8
|
+
# documents pass and the comparison may proceed.
|
|
9
|
+
module SchemaGate
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
# @param obj1 [String] first XML document
|
|
13
|
+
# @param obj2 [String] second XML document
|
|
14
|
+
# @param schema [String, Leptris::XML::XSD::Schema] schema text
|
|
15
|
+
# (compiled once here) or a compiled schema
|
|
16
|
+
# @return [Hash{Integer=>Canon::ValidationError}]
|
|
17
|
+
def gate(obj1, obj2, schema)
|
|
18
|
+
schema = Canon::Validators::XsdValidator.compile(schema) if schema.is_a?(String)
|
|
19
|
+
failures = {}
|
|
20
|
+
[obj1, obj2].each_with_index do |obj, side|
|
|
21
|
+
Canon::Validators::XsdValidator.validate!(obj, schema: schema)
|
|
22
|
+
rescue Canon::ValidationError => e
|
|
23
|
+
failures[side] = e
|
|
24
|
+
end
|
|
25
|
+
failures
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/canon/comparison.rb
CHANGED
|
@@ -95,6 +95,7 @@ module Canon
|
|
|
95
95
|
autoload :DiffNodeBuilder, "canon/comparison/diff_node_builder"
|
|
96
96
|
autoload :Dimensions, "canon/comparison/dimensions"
|
|
97
97
|
autoload :FormatDetector, "canon/comparison/format_detector"
|
|
98
|
+
autoload :SchemaGate, "canon/comparison/schema_gate"
|
|
98
99
|
autoload :HtmlComparator, "canon/comparison/html_comparator"
|
|
99
100
|
autoload :HtmlCompareProfile, "canon/comparison/html_compare_profile"
|
|
100
101
|
autoload :HtmlParser, "canon/comparison/html_parser"
|
|
@@ -246,6 +247,33 @@ module Canon
|
|
|
246
247
|
:semantic_tree))
|
|
247
248
|
end
|
|
248
249
|
|
|
250
|
+
# W5.1 schema gate: when a schema rides the options, both XML
|
|
251
|
+
# documents must pass the engine's XSD assessment BEFORE the
|
|
252
|
+
# diff runs — a validation failure is normative regardless of
|
|
253
|
+
# what the tree comparison would say.
|
|
254
|
+
if (xsd = opts[:xsd])
|
|
255
|
+
gate = SchemaGate.gate(obj1, obj2, xsd)
|
|
256
|
+
unless gate.empty?
|
|
257
|
+
diffs = gate.map do |side, error|
|
|
258
|
+
node = Diff::DiffNode.new(node1: side.zero? ? obj1 : obj2,
|
|
259
|
+
node2: side.zero? ? obj2 : obj1,
|
|
260
|
+
dimension: :schema_validation,
|
|
261
|
+
reason: error.message)
|
|
262
|
+
node.normative = true # a schema failure is normative by definition
|
|
263
|
+
node
|
|
264
|
+
end
|
|
265
|
+
return false unless opts[:verbose]
|
|
266
|
+
|
|
267
|
+
return ComparisonResult.new(
|
|
268
|
+
differences: diffs,
|
|
269
|
+
preprocessed_strings: [obj1.to_s, obj2.to_s],
|
|
270
|
+
format: opts[:format] || :xml,
|
|
271
|
+
algorithm: :dom,
|
|
272
|
+
original_strings: [obj1.to_s, obj2.to_s],
|
|
273
|
+
)
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
249
277
|
if %i[semantic semantic_tree].include?(opts[:diff_algorithm])
|
|
250
278
|
return semantic_diff(obj1, obj2, opts)
|
|
251
279
|
end
|
data/lib/canon/digest.rb
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
|
|
5
|
+
module Canon
|
|
6
|
+
# W5.3 — canonical digests: the hash of a document's canonical form
|
|
7
|
+
# as a first-class API for deduplication and change detection.
|
|
8
|
+
#
|
|
9
|
+
# The digest is taken over the format's CANONICAL form — the form
|
|
10
|
+
# under which the comparison pipeline deems documents equivalent:
|
|
11
|
+
# XML rides Canonical XML 1.1 (attribute order sorted,
|
|
12
|
+
# namespace-normalized); JSON/YAML ride the key-sorted forms; HTML
|
|
13
|
+
# rides the whitespace-law form with sorted attributes. Two
|
|
14
|
+
# documents that compare equivalent under the canonical form
|
|
15
|
+
# produce identical digests. The digests are stable across releases
|
|
16
|
+
# for as long as the canonical form is (the digest-stability suite
|
|
17
|
+
# pins the bytes per release pair).
|
|
18
|
+
module Digest
|
|
19
|
+
DEFAULT_ALGORITHM = "sha256"
|
|
20
|
+
|
|
21
|
+
class << self
|
|
22
|
+
# @param content [String] the document
|
|
23
|
+
# @param format [Symbol] the format (see SUPPORTED_FORMATS)
|
|
24
|
+
# @param algorithm [String, Symbol] any OpenSSL digest name
|
|
25
|
+
# @return [String] the hex digest of the canonical form
|
|
26
|
+
def digest(content, format, algorithm: DEFAULT_ALGORITHM)
|
|
27
|
+
OpenSSL::Digest.new(algorithm.to_s)
|
|
28
|
+
.hexdigest(canonical_form(content, format))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The canonical form the digest rides: XML's is C14N (the
|
|
32
|
+
# pretty-printed form preserves attribute order and is NOT
|
|
33
|
+
# canonical); every other format's formatter output IS the
|
|
34
|
+
# canonical form.
|
|
35
|
+
def canonical_form(content, format)
|
|
36
|
+
if format.to_sym == :xml
|
|
37
|
+
Formatters::XmlFormatter.format(content, pretty: false)
|
|
38
|
+
else
|
|
39
|
+
Canon.format(content, format)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "leptris" # the XSD lane rides the engine's tier-1 validator
|
|
4
|
+
# (libleptris >= 1.9.321, leptris#1075)
|
|
5
|
+
|
|
6
|
+
module Canon
|
|
7
|
+
module Validators
|
|
8
|
+
# XSD validation (W5.1) through the engine's tier-1 validator:
|
|
9
|
+
# schema compilation, whole-instance assessment, enumerated
|
|
10
|
+
# failures. The schema is a compiled `Leptris::XML::XSD::Schema`
|
|
11
|
+
# — callers validating many documents against one schema compile
|
|
12
|
+
# once and reuse (compilation is the expensive step).
|
|
13
|
+
#
|
|
14
|
+
# schema = Canon::Validators::XsdValidator.compile(schema_xml)
|
|
15
|
+
# Canon::Validators::XsdValidator.validate!(xml, schema: schema)
|
|
16
|
+
class XsdValidator < BaseValidator
|
|
17
|
+
# Compile a schema from XSD text. Raises Canon::ValidationError
|
|
18
|
+
# when the text is not well-formed XML, and when the engine
|
|
19
|
+
# reports schema-level problems.
|
|
20
|
+
#
|
|
21
|
+
# @param schema_xml [String] the XSD document
|
|
22
|
+
# @return [Leptris::XML::XSD::Schema] the compiled schema
|
|
23
|
+
def self.compile(schema_xml)
|
|
24
|
+
schema = Leptris::XML::XSD.compile(schema_xml)
|
|
25
|
+
if (detail = schema.error)
|
|
26
|
+
raise Canon::ValidationError.new(
|
|
27
|
+
"schema invalid: #{detail}",
|
|
28
|
+
format: :xml,
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
schema
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Validate an XML instance against a compiled schema
|
|
35
|
+
#
|
|
36
|
+
# @param input [String] the XML instance
|
|
37
|
+
# @param schema [Leptris::XML::XSD::Schema] a compiled schema
|
|
38
|
+
# @raise [Canon::ValidationError] if the instance fails
|
|
39
|
+
# assessment; the message carries the first failure, details
|
|
40
|
+
# carry the full enumerated list
|
|
41
|
+
# @return [void]
|
|
42
|
+
def self.validate!(input, schema:)
|
|
43
|
+
return if input.nil? || input.strip.empty?
|
|
44
|
+
|
|
45
|
+
doc = Leptris::XML.parse(input, readonly: true)
|
|
46
|
+
errors = schema.validate_errors(doc)
|
|
47
|
+
return if errors.empty?
|
|
48
|
+
|
|
49
|
+
raise Canon::ValidationError.new(
|
|
50
|
+
errors.first.to_s,
|
|
51
|
+
format: :xml,
|
|
52
|
+
details: errors.join("\n"),
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
data/lib/canon/validators.rb
CHANGED
|
@@ -7,6 +7,7 @@ module Canon
|
|
|
7
7
|
autoload :HtmlValidator, "canon/validators/html_validator"
|
|
8
8
|
autoload :JsonValidator, "canon/validators/json_validator"
|
|
9
9
|
autoload :XmlValidator, "canon/validators/xml_validator"
|
|
10
|
+
autoload :XsdValidator, "canon/validators/xsd_validator"
|
|
10
11
|
autoload :YamlValidator, "canon/validators/yaml_validator"
|
|
11
12
|
end
|
|
12
13
|
end
|
data/lib/canon/version.rb
CHANGED
|
@@ -55,12 +55,30 @@ module Canon
|
|
|
55
55
|
# — every node in the subtrees rooted at pfx:name. Returns
|
|
56
56
|
# [prefix, local_name], or nil for anything else.
|
|
57
57
|
def self.c14n_selection_idiom(xpath)
|
|
58
|
-
#
|
|
59
|
-
# expression (with prefix declarations and inline comments)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
58
|
+
# The suite's .xpath files are XML documents wrapping the
|
|
59
|
+
# expression (with prefix declarations and inline comments).
|
|
60
|
+
# Extraction is index-based string surgery — the CodeQL
|
|
61
|
+
# polynomial-redos query flags the gsub-chain form.
|
|
62
|
+
close_tag = xpath.rindex("</XPath>")
|
|
63
|
+
text = close_tag ? xpath[0...close_tag] : xpath.dup
|
|
64
|
+
# drop the XML declaration and the <XPath> open tag: each
|
|
65
|
+
# ends at the first '>' at/after its '<'
|
|
66
|
+
if (decl = text.index("<"))
|
|
67
|
+
gt = text.index(">", decl)
|
|
68
|
+
if (xopen = text.index("<XPath", decl)) && (xopen < gt)
|
|
69
|
+
gt = text.index(">", xopen)
|
|
70
|
+
end
|
|
71
|
+
text = text[(gt + 1)..] if gt
|
|
72
|
+
end
|
|
73
|
+
# strip <!-- --> comments by index (no regex backtracking)
|
|
74
|
+
loop do
|
|
75
|
+
open_c = text.index("<!--")
|
|
76
|
+
break unless open_c
|
|
77
|
+
|
|
78
|
+
close_c = text.index("-->", open_c + 4)
|
|
79
|
+
text = close_c ? text[0...open_c] + text[(close_c + 3)..] : text[0...open_c]
|
|
80
|
+
end
|
|
81
|
+
text = text.squeeze(" ")
|
|
64
82
|
m = text.match(%r{\(//\.\s*\|\s*//@\*\s*\|\s*//namespace::\*\)\s*\[\s*ancestor-or-self::([A-Za-z_][\w.-]*)(?::([\w.-]+))?\s*\]\s*\z})
|
|
65
83
|
return nil unless m
|
|
66
84
|
|
data/lib/canon.rb
CHANGED
|
@@ -32,6 +32,7 @@ module Canon
|
|
|
32
32
|
autoload :Cli, "canon/cli"
|
|
33
33
|
autoload :ColorDetector, "canon/color_detector"
|
|
34
34
|
autoload :DiffFormatter, "canon/diff_formatter"
|
|
35
|
+
autoload :Digest, "canon/digest"
|
|
35
36
|
autoload :Json, "canon/json"
|
|
36
37
|
|
|
37
38
|
SUPPORTED_FORMATS = %i[xml yaml json html html4 html5 string].freeze
|
|
@@ -84,5 +85,15 @@ module Canon
|
|
|
84
85
|
define_singleton_method("format_#{format}") do |content|
|
|
85
86
|
format(content, format)
|
|
86
87
|
end
|
|
88
|
+
|
|
89
|
+
define_singleton_method("digest_#{format}") do |content, algorithm: Canon::Digest::DEFAULT_ALGORITHM|
|
|
90
|
+
Canon::Digest.digest(content, format, algorithm: algorithm)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# The canonical digest of a document (W5.3): the hash of its
|
|
95
|
+
# canonical form. See Canon::Digest for the stability contract.
|
|
96
|
+
def self.digest(content, format = :xml, algorithm: Canon::Digest::DEFAULT_ALGORITHM)
|
|
97
|
+
Canon::Digest.digest(content, format, algorithm: algorithm)
|
|
87
98
|
end
|
|
88
99
|
end
|
|
@@ -30,16 +30,33 @@ class PerformanceComparator
|
|
|
30
30
|
DIM = "\e[2m"
|
|
31
31
|
CLEAR = "\e[0m"
|
|
32
32
|
|
|
33
|
+
BEST_OF = Integer(ENV.fetch("CANON_PERF_BEST_OF", "3"))
|
|
34
|
+
|
|
33
35
|
def run
|
|
34
36
|
clone_base
|
|
35
|
-
current =
|
|
36
|
-
base =
|
|
37
|
+
current = best_of(REPO_ROOT, "current")
|
|
38
|
+
base = best_of(base_clone_dir, "base (#{DEFAULT_BASE})")
|
|
37
39
|
print_report(current, base)
|
|
38
40
|
exit(1) if regressions?(current, base)
|
|
39
41
|
ensure
|
|
40
42
|
cleanup
|
|
41
43
|
end
|
|
42
44
|
|
|
45
|
+
# The html_parsing lanes drift up to ~27% BETWEEN SAME-CONTENT runs
|
|
46
|
+
# on shared runners (main failed against itself: run 40047205814) —
|
|
47
|
+
# single-run comparisons measure the runner, not the code. Each side
|
|
48
|
+
# runs BEST_OF times; per benchmark, the run with the highest upper
|
|
49
|
+
# bound stands (the capability estimate a fair gate compares).
|
|
50
|
+
def best_of(working_dir, label)
|
|
51
|
+
runs = Array.new(BEST_OF) { |i| run_report(working_dir, "#{label} ##{i + 1}") }
|
|
52
|
+
runs.first.fetch("benchmarks").keys.each_with_object({}) do |bench, merged|
|
|
53
|
+
best = runs.max_by { |r| r.fetch("benchmarks").fetch(bench).fetch("upper").to_f }
|
|
54
|
+
merged[bench] = best.fetch("benchmarks").fetch(bench)
|
|
55
|
+
end.then do |benchmarks|
|
|
56
|
+
runs.first.merge("benchmarks" => benchmarks)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
43
60
|
private
|
|
44
61
|
|
|
45
62
|
def clone_base
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: canon
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.117
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-24 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: diff-lcs
|
|
@@ -173,8 +173,10 @@ files:
|
|
|
173
173
|
- docs/Gemfile
|
|
174
174
|
- docs/INDEX.adoc
|
|
175
175
|
- docs/_config.yml
|
|
176
|
+
- docs/_references/canonical-digests.adoc
|
|
176
177
|
- docs/_references/comparison-theory.adoc
|
|
177
178
|
- docs/_references/html-canonical-form.adoc
|
|
179
|
+
- docs/_references/jcs-rfc8785.adoc
|
|
178
180
|
- docs/_references/yaml-canonical-form.adoc
|
|
179
181
|
- docs/advanced/diff-classification.adoc
|
|
180
182
|
- docs/advanced/diff-pipeline.adoc
|
|
@@ -267,6 +269,7 @@ files:
|
|
|
267
269
|
- lib/canon/comparison/pipeline.rb
|
|
268
270
|
- lib/canon/comparison/profile_definition.rb
|
|
269
271
|
- lib/canon/comparison/ruby_object_comparator.rb
|
|
272
|
+
- lib/canon/comparison/schema_gate.rb
|
|
270
273
|
- lib/canon/comparison/strategies.rb
|
|
271
274
|
- lib/canon/comparison/strategies/base_match_strategy.rb
|
|
272
275
|
- lib/canon/comparison/strategies/match_strategy_factory.rb
|
|
@@ -338,6 +341,7 @@ files:
|
|
|
338
341
|
- lib/canon/diff_formatter/legend.rb
|
|
339
342
|
- lib/canon/diff_formatter/pretty_diff_formatter.rb
|
|
340
343
|
- lib/canon/diff_formatter/theme.rb
|
|
344
|
+
- lib/canon/digest.rb
|
|
341
345
|
- lib/canon/errors.rb
|
|
342
346
|
- lib/canon/formatters.rb
|
|
343
347
|
- lib/canon/formatters/html4_formatter.rb
|
|
@@ -404,6 +408,7 @@ files:
|
|
|
404
408
|
- lib/canon/validators/html_validator.rb
|
|
405
409
|
- lib/canon/validators/json_validator.rb
|
|
406
410
|
- lib/canon/validators/xml_validator.rb
|
|
411
|
+
- lib/canon/validators/xsd_validator.rb
|
|
407
412
|
- lib/canon/validators/yaml_validator.rb
|
|
408
413
|
- lib/canon/version.rb
|
|
409
414
|
- lib/canon/xml.rb
|