Module: McpDiff::Render

Defined in:
lib/mcp_diff/render.rb

Overview

Output renderers: pretty (default), --json, --md. Findings always ordered most-severe first, then rule, then target. Port of cli/src/render.ts.

Constant Summary collapse

GLYPHS =
{ "breaking" => "", "behavior" => "", "compat" => "", "info" => "" }.freeze
COLORS =
{ "breaking" => 31, "behavior" => 33, "compat" => 32, "info" => 34 }.freeze
SEVERITY_RANK =
{ "breaking" => 0, "behavior" => 1, "compat" => 2, "info" => 3 }.freeze

Class Method Summary collapse

Class Method Details

.bold(str) ⇒ Object



23
24
25
# File 'lib/mcp_diff/render.rb', line 23

def bold(str)
  color? ? "\e[1m#{str}\e[22m" : str
end

.color?Boolean

Returns:

  • (Boolean)


15
16
17
# File 'lib/mcp_diff/render.rb', line 15

def color?
  ENV["NO_COLOR"].nil? && $stdout.tty?
end

.dim(str) ⇒ Object



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

def dim(str)
  color? ? "\e[2m#{str}\e[22m" : str
end

.paint(code, str) ⇒ Object



19
20
21
# File 'lib/mcp_diff/render.rb', line 19

def paint(code, str)
  color? ? "\e[#{code}m#{str}\e[39m" : str
end

.render(format, findings, exit_code) ⇒ Object



35
36
37
38
39
40
41
42
# File 'lib/mcp_diff/render.rb', line 35

def render(format, findings, exit_code)
  sorted = sort_findings(findings)
  case format
  when "json" then render_json(sorted, exit_code)
  when "md" then render_markdown(sorted, exit_code)
  else render_pretty(sorted, exit_code)
  end
end

.render_json(findings, exit_code) ⇒ Object



61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/mcp_diff/render.rb', line 61

def render_json(findings, exit_code)
  count = ->(cls) { findings.count { |f| f["class"] == cls } }
  JSON.pretty_generate(
    "findings" => findings,
    "summary" => {
      "breaking" => count.call("breaking"),
      "behavior" => count.call("behavior"),
      "compat" => count.call("compat"),
      "info" => count.call("info")
    },
    "exitCode" => exit_code
  )
end

.render_markdown(findings, exit_code) ⇒ Object



75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/mcp_diff/render.rb', line 75

def render_markdown(findings, exit_code)
  return "✅ **mcp-diff**: contract unchanged" if findings.empty?

  rows = findings.map do |f|
    "| #{GLYPHS[f['class']]} #{f['class'].upcase} | `#{f['ruleId']}` | `#{f['target']}` | #{f['message']} |"
  end
  header =
    case exit_code
    when 1 then "❌ **mcp-diff**: breaking contract changes"
    when 2 then "⚠️ **mcp-diff**: behavior-level contract changes"
    else "✅ **mcp-diff**: compatible contract changes"
    end
  [header, "", "| Severity | Rule | Target | Change |", "|---|---|---|---|", *rows].join("\n")
end

.render_pretty(findings, exit_code) ⇒ Object



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/mcp_diff/render.rb', line 44

def render_pretty(findings, exit_code)
  return paint(COLORS["compat"], "✓ contract unchanged") if findings.empty?

  rule_width = findings.map { |f| f["ruleId"].length }.max + 2
  lines = findings.map do |f|
    code = COLORS[f["class"]]
    [
      paint(code, GLYPHS[f["class"]]),
      paint(code, f["class"].upcase.ljust(9)),
      f["ruleId"].ljust(rule_width),
      "#{bold(f['target'])}: #{f['message']}"
    ].join(" ")
  end
  lines << dim("exit code #{exit_code}")
  lines.join("\n")
end

.sort_findings(findings) ⇒ Object



31
32
33
# File 'lib/mcp_diff/render.rb', line 31

def sort_findings(findings)
  findings.sort_by { |f| [SEVERITY_RANK[f["class"]], f["ruleId"], f["target"]] }
end