kairos-chain 3.43.0 → 3.46.0

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 (26) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +66 -0
  3. data/lib/kairos_mcp/version.rb +1 -1
  4. data/templates/skillsets/synoptis/bin/khab_verify.rb +172 -0
  5. data/templates/skillsets/synoptis/bin/map_verify.rb +250 -0
  6. data/templates/skillsets/synoptis/bin/rpr_verify.rb +171 -0
  7. data/templates/skillsets/synoptis/lib/synoptis/anchoring/anchoring_rule.rb +184 -0
  8. data/templates/skillsets/synoptis/lib/synoptis/anchoring/attestation_types.rb +107 -0
  9. data/templates/skillsets/synoptis/lib/synoptis/anchoring/chain_credential.rb +187 -0
  10. data/templates/skillsets/synoptis/lib/synoptis/anchoring/containment.rb +17 -1
  11. data/templates/skillsets/synoptis/lib/synoptis/anchoring/conventions/khab-1.md +112 -0
  12. data/templates/skillsets/synoptis/lib/synoptis/anchoring/conventions/map-1.md +198 -0
  13. data/templates/skillsets/synoptis/lib/synoptis/anchoring/conventions/rpr-1.md +141 -0
  14. data/templates/skillsets/synoptis/lib/synoptis/anchoring/cumulative_commitment.rb +229 -0
  15. data/templates/skillsets/synoptis/lib/synoptis/anchoring/deposit_board.rb +4 -2
  16. data/templates/skillsets/synoptis/lib/synoptis/anchoring/entry.rb +28 -1
  17. data/templates/skillsets/synoptis/lib/synoptis/anchoring/head_binding.rb +294 -0
  18. data/templates/skillsets/synoptis/lib/synoptis/anchoring/log.rb +15 -3
  19. data/templates/skillsets/synoptis/lib/synoptis/anchoring/public_verifier.rb +5 -0
  20. data/templates/skillsets/synoptis/lib/synoptis/anchoring/reproduction.rb +316 -0
  21. data/templates/skillsets/synoptis/lib/synoptis/anchoring/succession.rb +222 -0
  22. data/templates/skillsets/synoptis/lib/synoptis/anchoring/write_path.rb +5 -2
  23. data/templates/skillsets/synoptis/test/test_head_anchor.rb +431 -0
  24. data/templates/skillsets/synoptis/test/test_map_anchoring.rb +359 -0
  25. data/templates/skillsets/synoptis/test/test_rpr_reproduction.rb +238 -0
  26. metadata +17 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4ef5b418a3202426bfd13d7f62a849363f52144978f6b091c9ebb8bb43564927
4
- data.tar.gz: 471e635837cc5e5b65cd704dd78a9b48bd7312f61f7a27aa420c5323c5c0d43b
3
+ metadata.gz: 372dd921a298d45c97b957ae724a253bb22da73b5bb211025fe894eab5de4ff7
4
+ data.tar.gz: f15e37a2caeef2d4d08b92f49608aa52307b1a93124a1742d3f40c10ae145bf8
5
5
  SHA512:
6
- metadata.gz: e45299ac0b7b574419742d6b8243fbd9b492641c37e5f5f1e81e22ea77623e2cf986ca96da63dfdde8cc5f08bef07ff83bb42ab816296c4fe50a002d80805ae5
7
- data.tar.gz: 8138a96211b233b85f9e6342a32f2a02745dbf230ad319c274880c659c444ad03bf1909013864a79abeab2dccba7d754edbd3350506e7ffd751b112aac2803b0
6
+ metadata.gz: c9fc60d9acee8dcb8b7a2bfb43021d3c8b129cb94471fe8488092683ade4755665faa72860fb8c1bed77d254e977226c3d8cd85bec4dfe95eea2d0dc12352190
7
+ data.tar.gz: e5769c4b23ce1daef9704423f41e097d8969f5b86442ab1592beab4e3dc7623a3a79e30aba51d7ca2d5621e55fd635db740b5489761995bc2b9b33de8eb0b80e
data/CHANGELOG.md CHANGED
@@ -4,6 +4,72 @@ All notable changes to the `kairos-chain` gem will be documented in this file.
4
4
 
5
5
  This project follows [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [3.46.0] - 2026-07-22
8
+
9
+ ### Synoptis — reproduction endorsement rpr-1 (AUD-L3 slice 1)
10
+
11
+ Implements AUD-L3 design v0.4 (RPR-1..5, review-converged) slice 1 in the
12
+ synoptis SkillSet. All additive: rpr-1 builds on map-1/khab-1 exactly as
13
+ map-1 built on khab-1; no pre-existing file changed.
14
+
15
+ - `conventions/rpr-1.md`: content-addressed reproduction-endorsement
16
+ convention — re-execution target (inputs/environment/pipeline/output
17
+ digests; the first three form the committed computation identification,
18
+ output excluded), target-bound result-free tolerance declaration
19
+ (bit-identity at rpr-1), reproduction endorsement carrying the adjudicated
20
+ verdict (reproduced / not-reproduced) and its adjudication mode
21
+ (hand / procedure), declaration-set assessment (order-independent, sibling
22
+ targets pooled by computation identification, residue disclosed), map-1 §3
23
+ retraction reused unchanged.
24
+ - `reproduction.rb`: build/parse/digest for target, tolerance, endorsement
25
+ (closed schemas, canonical-serialization equality, refuse-not-coerce);
26
+ endorsement signing/verification via the unchanged map-1 §1.1 attestation
27
+ signature (ChainCredential); foreignness conformance check (RPR-4);
28
+ declaration-set assessment reporting invoked commitment (earliest
29
+ anterior), posterior commitments, digest-tiebroken rank, distinct-digest
30
+ multiplicity, and unresolved-equivalence residue.
31
+ - `bin/rpr_verify.rb`: offline auditor verifier (target / tolerance /
32
+ endorsement / assess / convention). Strict intake (at most one trailing
33
+ LF via delete_suffix — CRLF refused), canonical base-10 numerals only,
34
+ exit-code discipline (0 verified/report, 1 rejected, 2 unresolvable),
35
+ RPR-1/RPR-4 condition disclosure in output.
36
+ - `test/test_rpr_reproduction.rb`: 37 design-constraint assertions
37
+ (RPR-1..5), including presentation-order independence of the assessment
38
+ and rank, CRLF/padding refusal, and map-1 non-interference (75/75
39
+ regression green).
40
+
41
+ Implementation review: 3 rounds, R3 5/5 APPROVE, zero remaining P0/P1
42
+ (executable-probe personas; dispositions in docs/drafts/).
43
+
44
+ ## [3.45.0] - 2026-07-21
45
+
46
+ ### Synoptis — mutual anchoring protocol MAP-1..4 (map-1, AUD-L2)
47
+
48
+ Implements AUD-L2 design v0.5 (FROZEN) slice 1 in the synoptis SkillSet:
49
+
50
+ - `conventions/map-1.md`: content-addressed mutual-anchoring convention
51
+ (chain identity credential, typed attestation vocabulary, succession
52
+ records, declared anchoring rule, changeover event). Builds on khab-1
53
+ without modifying it.
54
+ - `chain_credential.rb`: Ed25519 self-authenticating chain identity
55
+ credential — verification needs no registry and no network.
56
+ - `succession.rb`: succession governance (earliest non-retracted designation
57
+ governs; at-or-before changeover boundary; issuer-only retraction;
58
+ retract-and-redesignate trails surfaced in the contested register).
59
+ - `anchoring_rule.rb`: result-free declared anchoring rule (closed schema)
60
+ with a coverage checker that reports and never enforces.
61
+ - `attestation_types.rb`: MAP-4 vocabulary intake validation and anchor-log
62
+ retraction coherence (issuer = depositor at map-1, disclosed deferral).
63
+ - `entry.rb` / `log.rb` / `write_path.rb`: additive `attestation_type` on
64
+ newly appended entries only (AHM-4 byte-invariance preserved).
65
+ - `bin/map_verify.rb`: standalone offline verifier (credential, attestation,
66
+ succession, coverage, pair, retraction subcommands).
67
+ - `test/test_map_anchoring.rb`: 75 design-constraint tests.
68
+
69
+ Design review converged R5 5/6 APPROVE; implementation review converged
70
+ R4 5/5 APPROVE with zero residual P0/P1. The inaugural mutual anchor with a
71
+ second, independently operated HestiaChain instance is deliberately pending.
72
+
7
73
  ## [3.42.2] - 2026-07-14
8
74
 
9
75
  ### Knowledge — loop_engineering_patterns §B: Model/Effort budget allocation
@@ -1,4 +1,4 @@
1
1
  module KairosMcp
2
- VERSION = "3.43.0"
2
+ VERSION = "3.46.0"
3
3
  CHANGELOG_URL = "https://github.com/masaomi/KairosChain_2026/blob/main/CHANGELOG.md"
4
4
  end
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # khab_verify — offline auditor verifier for khab-1 head bindings (MPR-4).
5
+ #
6
+ # Verifies inclusion and consistency proofs with EXACTLY the disclosed trust
7
+ # base: the proof artifact, the published head binding(s), and the shipped
8
+ # khab-1 convention definition. No internal-chain access, no operator
9
+ # cooperation, no network. (The authentic anchor-log view — that the binding
10
+ # really is committed at its claimed position — is checked against the log's
11
+ # public verification surface, outside this script.)
12
+ #
13
+ # Usage:
14
+ # khab_verify.rb inclusion <proof.json> <binding.json>
15
+ # khab_verify.rb consistency <proof.json> <earlier_binding.json> <later_binding.json>
16
+ #
17
+ # A binding file may be the bare head_binding object, or any JSON object
18
+ # carrying it under "head_binding" (e.g. a public-verifier record or an anchor
19
+ # entry body).
20
+ #
21
+ # Exit status: 0 = VERIFIED, 1 = REJECTED, 2 = usage / unresolvable input.
22
+
23
+ require 'json'
24
+ require 'digest'
25
+ require_relative '../lib/synoptis/anchoring/cumulative_commitment'
26
+
27
+ CONVENTION_PATH = File.expand_path('../lib/synoptis/anchoring/conventions/khab-1.md', __dir__)
28
+ CC = Synoptis::Anchoring::CumulativeCommitment
29
+
30
+ def die(msg)
31
+ warn "khab_verify: #{msg}"
32
+ exit 2
33
+ end
34
+
35
+ def reject(msg)
36
+ puts "REJECTED: #{msg}"
37
+ exit 1
38
+ end
39
+
40
+ def load_json(path)
41
+ parsed = JSON.parse(File.read(path))
42
+ die "#{path}: expected a JSON object, got #{parsed.class}" unless parsed.is_a?(Hash)
43
+ parsed
44
+ rescue JSON::ParserError, SystemCallError => e
45
+ die "cannot read #{path}: #{e.message}"
46
+ end
47
+
48
+ def extract_binding(obj, path)
49
+ b = obj.is_a?(Hash) && obj.key?('head_binding') ? obj['head_binding'] : obj
50
+ die "#{path} carries no head binding" unless b.is_a?(Hash) && b['convention']
51
+ b
52
+ end
53
+
54
+ # Structural well-formedness of a binding's verifiable fields, checked BEFORE
55
+ # any verdict logic. The strongest claims this verifier can emit (VERIFIED,
56
+ # DIVERGENCE) must be unreachable from malformed input: a missing or
57
+ # non-canonical root/extent is unresolvable input (exit 2), never evidence.
58
+ def require_wellformed!(binding, path)
59
+ unless binding['cumulative_root'].is_a?(String) && binding['cumulative_root'].match?(/\A[a-f0-9]{64}\z/)
60
+ die "#{path}: cumulative_root missing or not 64-char lowercase hex (khab-1 §4)"
61
+ end
62
+ unless binding['tree_size'].is_a?(Integer) && binding['tree_size'].positive? && binding['tree_size'] < 2**53
63
+ die "#{path}: tree_size missing or not a positive JSON-safe integer (khab-1 §4)"
64
+ end
65
+ return if binding['chain_identity'].is_a?(String) &&
66
+ binding['chain_identity'].match?(/\Ablock1-sha256:[a-f0-9]{64}\z/)
67
+
68
+ die "#{path}: chain_identity missing or not in khab-1 §5 canonical form (block1-sha256:<64-hex>)"
69
+ end
70
+
71
+ # MPR-3: the convention identifier must resolve to a definition whose own
72
+ # integrity the auditor can verify. A digest mismatch means the binding was
73
+ # computed under a definition this verifier does not hold — unresolvable, so
74
+ # verification refuses rather than guessing.
75
+ def check_convention!(binding)
76
+ reject "unknown convention #{binding['convention'].inspect} (this verifier implements khab-1)" unless
77
+ binding['convention'] == 'khab-1'
78
+ local = begin
79
+ Digest::SHA256.hexdigest(File.binread(CONVENTION_PATH))
80
+ rescue SystemCallError => e
81
+ die "khab-1 convention definition unreadable at #{CONVENTION_PATH}: #{e.message}"
82
+ end
83
+ return if binding['convention_sha256'] == local
84
+
85
+ reject "convention_sha256 #{binding['convention_sha256']} does not match the held khab-1 " \
86
+ "definition (#{local}); convention unresolvable"
87
+ end
88
+
89
+ def require_match(proof, binding, proof_key, binding_key, label)
90
+ return if proof[proof_key] == binding[binding_key]
91
+
92
+ reject "proof #{label} #{proof[proof_key].inspect} does not match binding #{binding[binding_key].inspect}"
93
+ end
94
+
95
+ mode = ARGV.shift
96
+
97
+ case mode
98
+ when 'inclusion'
99
+ die 'usage: khab_verify.rb inclusion <proof.json> <binding.json>' unless ARGV.size == 2
100
+ proof = load_json(ARGV[0])
101
+ binding = extract_binding(load_json(ARGV[1]), ARGV[1])
102
+ check_convention!(binding)
103
+ require_wellformed!(binding, ARGV[1])
104
+ reject "proof format #{proof['format'].inspect} is not khab-1/inclusion" unless proof['format'] == 'khab-1/inclusion'
105
+ require_match(proof, binding, 'chain_identity', 'chain_identity', 'chain_identity')
106
+ require_match(proof, binding, 'tree_size', 'tree_size', 'tree_size')
107
+ require_match(proof, binding, 'cumulative_root', 'cumulative_root', 'cumulative_root')
108
+
109
+ ok = CC.verify_inclusion(
110
+ record_commitment: proof['record_commitment'],
111
+ index: proof['index'],
112
+ tree_size: binding['tree_size'],
113
+ path: proof['path'],
114
+ root: binding['cumulative_root']
115
+ )
116
+ reject 'inclusion proof does not recompute the committed cumulative root' unless ok
117
+
118
+ puts 'VERIFIED: record commitment ' \
119
+ "#{proof['record_commitment']} is member ##{proof['index']} of the anchored state " \
120
+ "(tree_size #{binding['tree_size']}, chain #{binding['chain_identity']})."
121
+ puts 'Scope (MPR-5/6): membership, integrity at anchor time, position, order — nothing about ' \
122
+ 'content quality or completeness of anchoring; commitment, not content.'
123
+
124
+ when 'consistency'
125
+ die 'usage: khab_verify.rb consistency <proof.json> <earlier.json> <later.json>' unless ARGV.size == 3
126
+ proof = load_json(ARGV[0])
127
+ earlier = extract_binding(load_json(ARGV[1]), ARGV[1])
128
+ later = extract_binding(load_json(ARGV[2]), ARGV[2])
129
+ check_convention!(earlier)
130
+ check_convention!(later)
131
+ require_wellformed!(earlier, ARGV[1])
132
+ require_wellformed!(later, ARGV[2])
133
+ reject "proof format #{proof['format'].inspect} is not khab-1/consistency" unless proof['format'] == 'khab-1/consistency'
134
+ # MPR-9: extension-relatedness is defined only over the same committed chain
135
+ # identity; different identities terminate the claim rather than failing it.
136
+ reject "bindings commit different chain identities (#{earlier['chain_identity']} vs " \
137
+ "#{later['chain_identity']}); extension claim terminated across an identity change" unless
138
+ earlier['chain_identity'] == later['chain_identity']
139
+ # Equal committed extent with different committed roots needs no proof at
140
+ # all: the two states differ at the same size, so no extension relation can
141
+ # exist. This is arithmetic on the committed bindings — a POSITIVE
142
+ # divergence witness (MPR-9), unlike a mere proof-verification failure.
143
+ if earlier['tree_size'] == later['tree_size'] && earlier['cumulative_root'] != later['cumulative_root']
144
+ puts "DIVERGENCE: both bindings commit chain #{later['chain_identity']} at extent " \
145
+ "#{later['tree_size']} with different cumulative roots; conclusive evidence of " \
146
+ 'rewriting or forking (MPR-9)'
147
+ exit 1
148
+ end
149
+ require_match(proof, earlier, 'first_root', 'cumulative_root', 'first_root')
150
+ require_match(proof, later, 'second_root', 'cumulative_root', 'second_root')
151
+ require_match(proof, earlier, 'first_size', 'tree_size', 'first_size')
152
+ require_match(proof, later, 'second_size', 'tree_size', 'second_size')
153
+
154
+ ok = CC.verify_consistency(
155
+ first_root: earlier['cumulative_root'],
156
+ first_size: earlier['tree_size'],
157
+ second_root: later['cumulative_root'],
158
+ second_size: later['tree_size'],
159
+ path: proof['path']
160
+ )
161
+ # Design §11: failure of an operator-supplied proof is NOT by itself a
162
+ # positive inconsistency witness — report it as unestablished, not as forgery.
163
+ reject 'consistency proof does not verify; extension claim remains UNESTABLISHED ' \
164
+ '(not by itself evidence of rewriting — MPR-9)' unless ok
165
+
166
+ puts "VERIFIED: the later anchored state (tree_size #{later['tree_size']}) commits to an " \
167
+ "extension of the earlier state (tree_size #{earlier['tree_size']}) unchanged as a prefix " \
168
+ "(chain #{later['chain_identity']})."
169
+
170
+ else
171
+ die "usage: khab_verify.rb {inclusion|consistency} <proof.json> <binding(s).json...>"
172
+ end
@@ -0,0 +1,250 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # map_verify — offline auditor verifier for map-1 mutual-anchoring artifacts
5
+ # (MAP-1..4, aud_l2_mutual_anchoring_design v0.5).
6
+ #
7
+ # Verifies with EXACTLY the disclosed trust base: the artifacts named on the
8
+ # command line plus the shipped map-1 convention definition. No registry, no
9
+ # network, no operator cooperation. (Authentic log/chain views are the
10
+ # caller's inputs; their own integrity is checked by the khab-1 surface.)
11
+ #
12
+ # Usage:
13
+ # map_verify.rb credential <credential.json>
14
+ # map_verify.rb attestation <credential.json> <payload-file> <signature-hex>
15
+ # map_verify.rb succession <old_credential.json> <records.json> [changeover_position]
16
+ # records.json = JSON array of internal-chain record STRINGS, committed order
17
+ # map_verify.rb coverage <rule-file> <anchors.json> [chain_extent] [rule_position] [rule_moment] [now]
18
+ # anchors.json = JSON array of {"tree_size":..,"moment":".."} observations;
19
+ # every_n_records needs chain_extent (+ optional rule_position);
20
+ # every_n_days needs rule_moment and now (ISO8601) as args 5 and 6 —
21
+ # pass 0 0 for the unused args 3 and 4.
22
+ # map_verify.rb pair <log_a.json> <identity_a> <log_b.json> <identity_b>
23
+ # log_*.json = JSON array of anchor-entry objects (entry.to_h shape);
24
+ # reports what a verified pair establishes (MAP-1) — and ONLY that.
25
+ # map_verify.rb retraction <log.json> <retraction_entry_hash>
26
+ # checks map-1 §3 coherence of an anchor-log retraction against its target.
27
+ #
28
+ # Exit status: 0 = VERIFIED/REPORT, 1 = REJECTED, 2 = usage / unresolvable.
29
+
30
+ require 'json'
31
+ require 'digest'
32
+ require_relative '../lib/synoptis/anchoring/entry'
33
+ require_relative '../lib/synoptis/anchoring/chain_credential'
34
+ require_relative '../lib/synoptis/anchoring/succession'
35
+ require_relative '../lib/synoptis/anchoring/anchoring_rule'
36
+ require_relative '../lib/synoptis/anchoring/attestation_types'
37
+
38
+ ENTRY = Synoptis::Anchoring::Entry
39
+ CRED = Synoptis::Anchoring::ChainCredential
40
+ SUCC = Synoptis::Anchoring::Succession
41
+ RULE = Synoptis::Anchoring::AnchoringRule
42
+ ATYPE = Synoptis::Anchoring::AttestationTypes
43
+
44
+ def die(msg)
45
+ warn "map_verify: #{msg}"
46
+ exit 2
47
+ end
48
+
49
+ def reject(msg)
50
+ puts "REJECTED: #{msg}"
51
+ exit 1
52
+ end
53
+
54
+ def load_json(path)
55
+ JSON.parse(File.read(path))
56
+ rescue JSON::ParserError, SystemCallError => e
57
+ die "cannot read #{path}: #{e.message}"
58
+ end
59
+
60
+ # Malformed caller input is unresolvable (exit 2), never a verdict.
61
+ # Explicit base 10: Integer('010') would read octal and silently shift a
62
+ # governance boundary.
63
+ def strict_int(value, label)
64
+ # Digits only: Integer() would also accept '1_0' and padded whitespace.
65
+ die "#{label} must be a base-10 integer, got #{value.inspect}" unless value.is_a?(String) && value.match?(/\A\d+\z/)
66
+ Integer(value, 10)
67
+ end
68
+
69
+ def load_json_hash(path, label)
70
+ parsed = load_json(path)
71
+ die "#{path}: #{label} must be a JSON object, got #{parsed.class}" unless parsed.is_a?(Hash)
72
+ parsed
73
+ end
74
+
75
+ HEX64 = /\A[a-f0-9]{64}\z/
76
+
77
+ # Structural well-formedness of a matched head binding (khab-1 §4 shapes).
78
+ # The strongest claim this tool prints must be unreachable from malformed
79
+ # input: a garbage binding is unresolvable, never PAIR ESTABLISHED.
80
+ def require_binding_wellformed!(binding, where)
81
+ die "#{where}: head_binding.convention is #{binding['convention'].inspect}, not khab-1" unless binding['convention'] == 'khab-1'
82
+ %w[convention_sha256 cumulative_root].each do |k|
83
+ die "#{where}: head_binding.#{k} missing or not 64-char lowercase hex" unless binding[k].is_a?(String) && binding[k].match?(HEX64)
84
+ end
85
+ die "#{where}: head_binding.tree_size must be a positive Integer" unless binding['tree_size'].is_a?(Integer) && binding['tree_size'].positive?
86
+ unless binding['chain_identity'].is_a?(String) && binding['chain_identity'].match?(/\Ablock1-sha256:[a-f0-9]{64}\z/)
87
+ die "#{where}: head_binding.chain_identity is not block1-sha256:<64-hex> (khab-1 §5)"
88
+ end
89
+ end
90
+
91
+ # Recompute an entry's committed hash from its canonical content. Detects an
92
+ # in-place edited entry without any chain access; the full log hash chain is
93
+ # the khab-1 surface's business (khab_verify / public verifier).
94
+ def require_entry_hash!(entry_h, where)
95
+ content = { 'position' => entry_h['position'], 'prev' => entry_h['prev'],
96
+ 'kind' => entry_h['kind'], 'body' => entry_h['body'] }
97
+ recomputed = ENTRY.compute_hash(content)
98
+ return if recomputed == entry_h['entry_hash']
99
+
100
+ reject "#{where}: entry_hash does not recompute from committed content (edited in place?)"
101
+ end
102
+
103
+ mode = ARGV.shift
104
+
105
+ case mode
106
+ when 'credential'
107
+ die 'usage: map_verify.rb credential <credential.json>' unless ARGV.size == 1
108
+ cred = load_json_hash(ARGV[0], 'credential')
109
+ begin
110
+ CRED.validate!(cred)
111
+ rescue CRED::CredentialError => e
112
+ reject e.message
113
+ end
114
+ puts "VERIFIED: credential digest #{CRED.credential_digest(cred)} binds #{cred['chain_identity']}"
115
+ puts 'NOTE: binding is a SELF-attestation (map-1 §1); that the credential speaks'
116
+ puts 'for the chain requires the chain to commit the credential digest (map-1 §2).'
117
+
118
+ when 'attestation'
119
+ die 'usage: map_verify.rb attestation <credential.json> <payload-file> <signature-hex>' unless ARGV.size == 3
120
+ cred = load_json_hash(ARGV[0], 'credential')
121
+ payload = begin
122
+ File.binread(ARGV[1])
123
+ rescue SystemCallError => e
124
+ die "cannot read #{ARGV[1]}: #{e.message}"
125
+ end
126
+ begin
127
+ ok = CRED.verify_attestation(cred, payload, ARGV[2])
128
+ rescue CRED::CredentialError => e
129
+ reject e.message
130
+ end
131
+ reject 'attestation signature does not verify under the credential' unless ok
132
+ puts "VERIFIED: attestation signature valid under credential #{CRED.credential_digest(cred)[0, 12]}…"
133
+
134
+ when 'succession'
135
+ die 'usage: map_verify.rb succession <old_credential.json> <records.json> [changeover_position]' unless (2..3).cover?(ARGV.size)
136
+ cred = load_json_hash(ARGV[0], 'credential')
137
+ records = load_json(ARGV[1])
138
+ unless records.is_a?(Array) && records.all? { |r| r.is_a?(String) }
139
+ die "#{ARGV[1]}: expected a JSON array of record STRINGS (each element the exact committed record string)"
140
+ end
141
+ changeover = ARGV[2].nil? ? nil : strict_int(ARGV[2], 'changeover_position')
142
+ die 'changeover_position must be non-negative' if !changeover.nil? && changeover.negative?
143
+ begin
144
+ verdict = SUCC.governance(cred, records, changeover_position: changeover)
145
+ rescue CRED::CredentialError => e
146
+ reject e.message
147
+ end
148
+ puts JSON.pretty_generate(verdict)
149
+ puts 'NOTE: the verdict reports what the records show, never intent (key'
150
+ puts 'compromise is indistinguishable from the issuer — MAP-2 disclosed limit).'
151
+
152
+ when 'coverage'
153
+ unless (2..6).cover?(ARGV.size)
154
+ die 'usage: map_verify.rb coverage <rule-file> <anchors.json> [chain_extent] [rule_position] [rule_moment] [now]'
155
+ end
156
+ rule_string = begin
157
+ File.read(ARGV[0]).strip
158
+ rescue SystemCallError => e
159
+ die "cannot read #{ARGV[0]}: #{e.message}"
160
+ end
161
+ rule = begin
162
+ RULE.parse!(rule_string)
163
+ rescue RULE::RuleError => e
164
+ reject e.message
165
+ end
166
+ anchors = load_json(ARGV[1])
167
+ die "#{ARGV[1]}: expected a JSON array of observations" unless anchors.is_a?(Array)
168
+ extent = ARGV[2].nil? ? nil : strict_int(ARGV[2], 'chain_extent')
169
+ rule_pos = ARGV[3].nil? ? nil : strict_int(ARGV[3], 'rule_position')
170
+ rule_moment = ARGV[4]
171
+ now = ARGV[5]
172
+ # A tool capability gap is a usage error (exit 2), never a REJECTED verdict.
173
+ if rule['trigger'] == 'every_n_records' && extent.nil?
174
+ die 'every_n_records coverage needs chain_extent'
175
+ end
176
+ if rule['trigger'] == 'every_n_days' && (rule_moment.nil? || now.nil?)
177
+ die 'every_n_days coverage needs rule_moment and now (ISO8601), as arguments 5 and 6'
178
+ end
179
+ begin
180
+ report = RULE.coverage(rule_string, anchors, chain_extent: extent, rule_position: rule_pos,
181
+ rule_moment: rule_moment, now: now)
182
+ rescue RULE::RuleError => e
183
+ reject e.message
184
+ end
185
+ puts JSON.pretty_generate(report)
186
+
187
+ when 'pair'
188
+ die 'usage: map_verify.rb pair <log_a.json> <identity_a> <log_b.json> <identity_b>' unless ARGV.size == 4
189
+ log_a, id_a, log_b, id_b = load_json(ARGV[0]), ARGV[1], load_json(ARGV[2]), ARGV[3]
190
+ [[log_a, ARGV[0]], [log_b, ARGV[2]]].each do |(log, path)|
191
+ die "#{path}: expected a JSON array of anchor-entry objects" unless log.is_a?(Array)
192
+ end
193
+ find = lambda do |log, identity, where|
194
+ log.each_with_index.filter_map do |entry, i|
195
+ e = entry.is_a?(Hash) ? entry.transform_keys(&:to_s) : {}
196
+ # Only anchor entries carry head bindings; anything else shaped to look
197
+ # like one never counts toward a pair.
198
+ next unless e['kind'] == 'anchor'
199
+
200
+ body = e['body'].is_a?(Hash) ? e['body'] : {}
201
+ hb = body['head_binding']
202
+ next unless hb.is_a?(Hash) && hb['chain_identity'] == identity
203
+
204
+ require_binding_wellformed!(hb, "#{where} entry #{e['position'] || i}")
205
+ require_entry_hash!(e, "#{where} entry #{e['position'] || i}")
206
+ { position: e['position'] || i, cumulative_root: hb['cumulative_root'],
207
+ tree_size: hb['tree_size'], attestation_type: body['attestation_type'] }
208
+ end
209
+ end
210
+ a_holds_b = find.call(log_a, id_b, ARGV[0])
211
+ b_holds_a = find.call(log_b, id_a, ARGV[2])
212
+ reject "no entry in #{ARGV[0]} commits a head binding of #{id_b}" if a_holds_b.empty?
213
+ reject "no entry in #{ARGV[2]} commits a head binding of #{id_a}" if b_holds_a.empty?
214
+ puts 'PAIR ESTABLISHED (MAP-1 — exactly this, nothing stronger):'
215
+ puts " #{id_b} head(s) committed in log A at #{a_holds_b.map { |e| e[:position] }.join(', ')}"
216
+ puts " #{id_a} head(s) committed in log B at #{b_holds_a.map { |e| e[:position] }.join(', ')}"
217
+ untyped = (a_holds_b + b_holds_a).count { |e| e[:attestation_type].nil? }
218
+ puts " untyped (pre-map-1, grandfathered) inscriptions: #{untyped}" if untyped.positive?
219
+ puts 'CONDITIONS (travel with every stronger reading, map-1 §6):'
220
+ puts ' - partner independence is auditor-supplied; a common operator can fabricate the pair'
221
+ puts ' - temporal weight reaches only as far as each log\'s own khab-1 anchoring'
222
+ puts ' - equivocation detection requires authentic views of BOTH logs and cross-comparison'
223
+ puts ' - matched entry hashes recomputed and binding shapes checked here; full log hash-chain'
224
+ puts ' and cumulative-root verification are the khab-1 surface\'s business (khab_verify)'
225
+ puts ' - this pair supplies split-view detection material; it closes nothing'
226
+
227
+ when 'retraction'
228
+ die 'usage: map_verify.rb retraction <log.json> <retraction_entry_hash>' unless ARGV.size == 2
229
+ log = load_json(ARGV[0])
230
+ die "#{ARGV[0]}: expected a JSON array of anchor-entry objects" unless log.is_a?(Array)
231
+ entries = log.map { |e| e.is_a?(Hash) ? e.transform_keys(&:to_s) : {} }
232
+ retraction = entries.find { |e| e['entry_hash'] == ARGV[1] }
233
+ die "no entry with entry_hash #{ARGV[1]} in #{ARGV[0]}" if retraction.nil?
234
+ body = retraction['body'].is_a?(Hash) ? retraction['body'] : {}
235
+ meta = body['metadata'].is_a?(Hash) ? body['metadata'] : {}
236
+ target_ref = meta['target_entry_hash']
237
+ unless target_ref.is_a?(String) && target_ref.match?(HEX64)
238
+ reject "retraction carries no resolvable metadata.target_entry_hash (got #{target_ref.inspect})"
239
+ end
240
+ target = entries.find { |e| e['entry_hash'] == target_ref }
241
+ reject "target entry #{target_ref} not present in the supplied log view" if target.nil?
242
+ require_entry_hash!(retraction, 'retraction')
243
+ require_entry_hash!(target, 'target')
244
+ verdict = ATYPE.retraction_coherence(retraction, target)
245
+ puts JSON.pretty_generate(verdict)
246
+ exit(verdict[:coherent] ? 0 : 1)
247
+
248
+ else
249
+ die 'usage: map_verify.rb credential|attestation|succession|coverage|pair|retraction ... (see header)'
250
+ end