Module: Hecks::Ports::Agent::Answers

Defined in:
lib/hecks/ports/agent/answers.rb

Overview

WHERE A RAW HASH BECOMES A STRUCT — the one place, so every adapter (the real claude_code one, and any scripted double standing in for it in a spec) is held to the identical shape. An adapter's whole job ends at "here is what came back, already JSON"; whether that hash is USABLE is decided once, here, not re-decided per adapter.

Every failure raises ValidationError — never lets a bare NoMethodError on nil, or a KeyError, reach a caller looking for an agent-shaped failure. bin/interview catches exactly one class for "the model answered badly," not a grab-bag of Ruby's own.

Constant Summary collapse

VERB_PATTERN =
/\A[A-Za-z]+::[A-Za-z]+\.[A-Za-z]+\z/

Class Method Summary collapse

Class Method Details

.arguments!(row) ⇒ Object

ROWS SHAPED EXACTLY AS Interview::Proposal::Argument — a reference's field is legitimately blank (a reference IS an id), so only name is required here.



93
94
95
96
97
98
99
100
# File 'lib/hecks/ports/agent/answers.rb', line 93

def arguments!(row)
  Array(row["arguments"]).map do |argument|
    name = argument["name"].to_s
    raise ValidationError, "argument row has no name: #{argument.inspect}" if name.empty?

    { name: name, field: argument["field"].to_s, value: argument["value"].to_s }
  end
end

.findings(raw) ⇒ Object



31
32
33
34
35
36
# File 'lib/hecks/ports/agent/answers.rb', line 31

def findings(raw)
  rows(raw, "findings").map do |row|
    Finding.new(kind: kind!(row), severity: severity!(row), subject: text!(row, "subject"),
                message: text!(row, "message"))
  end
end

.kind!(row) ⇒ Object

Raises:



76
77
78
79
80
81
# File 'lib/hecks/ports/agent/answers.rb', line 76

def kind!(row)
  kind = row["kind"].to_s.to_sym
  return kind if CRITIQUE_KINDS.include?(kind)

  raise ValidationError, "#{kind.inspect} is not a critique kind this port knows (#{CRITIQUE_KINDS.join(', ')})"
end

.proposals(raw) ⇒ Object



25
26
27
28
29
# File 'lib/hecks/ports/agent/answers.rb', line 25

def proposals(raw)
  rows(raw, "proposals").map do |row|
    Proposal.new(verb: verb!(row), rationale: text!(row, "rationale"), arguments: arguments!(row))
  end
end

.questions(raw) ⇒ Object



21
22
23
# File 'lib/hecks/ports/agent/answers.rb', line 21

def questions(raw)
  rows(raw, "questions").map { |row| Question.new(text: text!(row, "text"), because: text!(row, "because")) }
end

.rows(raw, key) ⇒ Object

── shared checks, each named for what it refuses ───────────────

Raises:



47
48
49
50
51
52
53
54
# File 'lib/hecks/ports/agent/answers.rb', line 47

def rows(raw, key)
  raise ValidationError, "expected a Hash back, got #{raw.class}: #{raw.inspect}" unless raw.is_a?(Hash)

  value = raw[key]
  return value if value.is_a?(Array)

  raise ValidationError, "no #{key.inspect} array in the answer: #{raw.inspect}"
end

.severity!(row) ⇒ Object

Raises:



83
84
85
86
87
88
# File 'lib/hecks/ports/agent/answers.rb', line 83

def severity!(row)
  severity = row["severity"].to_s.to_sym
  return severity if SEVERITIES.include?(severity)

  raise ValidationError, "#{severity.inspect} is not a severity this port knows (#{SEVERITIES.join(', ')})"
end

.suggestions(raw) ⇒ Object



38
39
40
41
42
43
# File 'lib/hecks/ports/agent/answers.rb', line 38

def suggestions(raw)
  rows(raw, "names").map do |row|
    Suggestion.new(name: text!(row, "name"), because: text!(row, "because"),
                   rejected: Array(row["rejected"]).map(&:to_s))
  end
end

.text!(row, key) ⇒ Object

Raises:



56
57
58
59
60
61
# File 'lib/hecks/ports/agent/answers.rb', line 56

def text!(row, key)
  value = row[key]
  return value.to_s if value && !value.to_s.strip.empty?

  raise ValidationError, "#{key.inspect} missing or blank in #{row.inspect}"
end

.verb!(row) ⇒ Object

A VERB THE LANGUAGE COULD NOT EVEN PARSE IS AN ADAPTER FAULT, refused right here — the same not_fully_qualified shape the language's own grammar already refuses by, reused rather than reinvented. A verb naming a real category that turns out to describe the wrong fact is NOT this port's business: that one dispatches, and Interview::Session#offer is what says no.

Raises:



69
70
71
72
73
74
# File 'lib/hecks/ports/agent/answers.rb', line 69

def verb!(row)
  verb = row["verb"].to_s
  return verb if VERB_PATTERN.match?(verb)

  raise ValidationError, "#{verb.inspect} is not a fully-qualified verb (Chapter::Aggregate.Command)"
end