Module: McpDiff::Core::Canonical

Defined in:
lib/mcp_diff/core/canonical.rb

Overview

Deterministic serialization: sorted keys, array order preserved (it is meaningful), sha256 contract hash. Port of core/src/canonical.ts (D11).

Class Method Summary collapse

Class Method Details

.canonical_stringify(value) ⇒ Object

Canonical lockfile text: sorted keys, 2-space indent, trailing newline.



27
28
29
# File 'lib/mcp_diff/core/canonical.rb', line 27

def canonical_stringify(value)
  "#{JSON.pretty_generate(sort_keys_deep(value))}\n"
end

.contract_hash_of(snapshot_without_hash) ⇒ Object

sha256 of the snapshot's canonical (compact) form, hash field excluded.



37
38
39
40
# File 'lib/mcp_diff/core/canonical.rb', line 37

def contract_hash_of(snapshot_without_hash)
  compact = JSON.generate(sort_keys_deep(snapshot_without_hash))
  "sha256-#{Digest::SHA256.hexdigest(compact)}"
end

.deep_equal(a, b) ⇒ Object

Structural equality via canonical serialization (array order preserved).



32
33
34
# File 'lib/mcp_diff/core/canonical.rb', line 32

def deep_equal(a, b)
  JSON.generate(sort_keys_deep(a)) == JSON.generate(sort_keys_deep(b))
end

.sort_keys_deep(value) ⇒ Object

Recursively sort object keys; stringify keys so string/symbol inputs canonicalize identically. Array order is preserved.



15
16
17
18
19
20
21
22
23
24
# File 'lib/mcp_diff/core/canonical.rb', line 15

def sort_keys_deep(value)
  case value
  when Array
    value.map { |v| sort_keys_deep(v) }
  when Hash
    value.map { |k, v| [k.to_s, sort_keys_deep(v)] }.sort_by(&:first).to_h
  else
    value
  end
end