Module: Hecks::Ports::Agent
- Defined in:
- lib/hecks/ports/agent.rb,
lib/hecks/ports/agent/answers.rb
Overview
THE INTERVIEWER — whatever asks the next good question, reads a
sentence back into proposed declarations, judges a model on taste
rather than structure, or suggests a name. Resolved the singleton
way every other port here is (Ports::IdentityGeneration's own
zero/one/many — one interviewer per process, never per-aggregate),
because a second adapter answering this port is exactly as
unchoosable as a second one minting identities.
BUILT LAST, ON PURPOSE, AND STILL OPTIONAL. .claude/skills/ interview/SKILL.md conducts a whole session with NONE of this —
Claude Code reads bin/interview state itself, asks the human
directly with real conversation context, and calls bin/interview ask/propose/accept itself. This port exists for what that
cannot reach: a HEADLESS run (bin/interview suggest, no model in
the room otherwise), a batch critique over an already-written
chapter, and — the actual point of building it at all — a
SCRIPTED double (spec/fixtures/scripted_agent.rb) that makes the
whole loop testable with no real model involved.
WHAT THIS PORT IS NOT. It does not judge whether a declaration is
WELL-FORMED — that is the meta-domain's own job, answered by a real
dispatch refusing or not (Interview::Session#offer). This port
only ever produces SHAPE: a question, a proposed declaration, a
judgement about taste, a name. A proposal naming a category the
language does not declare is an ADAPTER fault, refused HERE, in
Answers; a proposal naming a real category but describing the
wrong domain fact is dispatched anyway, so the LANGUAGE gets to say
why. That line is the whole design.
PARSING BELONGS TO THIS FILE, NOT THE ADAPTER. An adapter answers with a plain, already-JSON-shaped Hash (a real model's parsed reply, or a spec double's own hand-built one) — never a Struct — so every adapter is validated identically here rather than trusting each one to refuse the same way. Two adapters that parsed differently would mean the SAME malformed answer passing through one and refusing through the other.
Defined Under Namespace
Modules: Answers Classes: Finding, Proposal, Question, Suggestion
Constant Summary collapse
- NAME =
"agent"- ValidationError =
The answer came back and could not be used — no JSON in it, a category the language does not declare, a severity outside the two that exist. A foreign failure (JSON::ParserError, a missing key) wrapped into something this port owns, the same shape
Ports::Authentication::ValidationErroralready is, so no caller ever rescues a raw parser error leaking out of an adapter. Class.new(StandardError)
Class.new(StandardError)
- CRITIQUE_KINDS =
CRITIQUE REUSES
Bluebook::ModelCheck::Finding'S OWN SHAPE — same fields, same severities, so a mechanical finding (Session#gaps) and a judgement (this) print in one list and sort together. The KIND vocabulary is NOT shared — ModelCheck's eleven kinds are structural facts about a graph; these are opinions about a model, closed here on purpose (an open kind field is an open prompt, and an open prompt drifts). %i[ anemic_aggregate wrong_boundary crud_verb leaky_value_object missing_lifecycle missing_invariant ubiquitous_language overreaching_identity untold_rule ].freeze
- SEVERITIES =
%i[error warning].freeze
Class Method Summary collapse
- .adapter(registry) ⇒ Object
-
.ask(registry, state:, asked: []) ⇒ Object
THE NEXT BEST QUESTION.
-
.critique(registry, declared:, refusals: [], findings: []) ⇒ Object
WHAT IS WRONG WITH THIS AS A MODEL — handed everything already known (the language's own refusals,
Session#gaps's mechanical findings) so it spends its judgement on what neither of those can see, rather than restating them. -
.interpret(registry, prose:, state:) ⇒ Object
PROSE -> PROPOSED DECLARATIONS.
-
.suggest_name(registry, meaning:, kind:, near: []) ⇒ Object
VOCABULARY HELP.
Class Method Details
.adapter(registry) ⇒ Object
149 150 151 152 153 154 155 156 157 158 159 160 161 162 |
# File 'lib/hecks/ports/agent.rb', line 149 def adapter(registry) implementations = registry.adapters.values.select { |a| a.port == NAME } case implementations.size when 1 then Adapters.const_get(implementations.first.name) when 0 raise Runtime::WiringError, "no adapter implements the #{NAME} port — nothing can conduct an interview" else raise Runtime::WiringError, "#{implementations.size} adapters implement the #{NAME} port " \ "(#{implementations.map(&:name).sort.join(', ')}) — the runtime will not choose for you" end end |
.ask(registry, state:, asked: []) ⇒ Object
THE NEXT BEST QUESTION. state is the whole picture — normally
Interview::Session#declaration plus #gaps, so an adapter
needs no memory of its own between calls; that is what lets a
headless run be a series of one-shot processes, same as
bin/interview itself already is.
113 114 115 |
# File 'lib/hecks/ports/agent.rb', line 113 def ask(registry, state:, asked: []) Answers.questions(adapter(registry).ask(state: state, asked: asked)) end |
.critique(registry, declared:, refusals: [], findings: []) ⇒ Object
WHAT IS WRONG WITH THIS AS A MODEL — handed everything already
known (the language's own refusals, Session#gaps's mechanical
findings) so it spends its judgement on what neither of those
can see, rather than restating them.
128 129 130 |
# File 'lib/hecks/ports/agent.rb', line 128 def critique(registry, declared:, refusals: [], findings: []) Answers.findings(adapter(registry).critique(declared: declared, refusals: refusals, findings: findings)) end |
.interpret(registry, prose:, state:) ⇒ Object
PROSE -> PROPOSED DECLARATIONS. Returns [] when the sentence
carried no declaration at all (a clarifying question back from
the human, say) — a legitimate answer, not a failure.
120 121 122 |
# File 'lib/hecks/ports/agent.rb', line 120 def interpret(registry, prose:, state:) Answers.proposals(adapter(registry).interpret(prose: prose, state: state)) end |
.suggest_name(registry, meaning:, kind:, near: []) ⇒ Object
VOCABULARY HELP. near is what the chapter already calls
things, so a suggestion cannot collide with a name in use.
NAMED suggest_name, NOT name — the plan's own word for this
operation, but Module#name already exists and is load-bearing
everywhere (backtraces, RSpec's own description building,
inspect): a module-function called name SHADOWS it the
instant it's defined, and SomeModule.name (no args) then
raises ArgumentError: missing keywords the next time anything
— not this file, something else entirely — asks the module its
own name. Measured, not theoretical: this exact collision broke
RSpec's own failure-message formatting the first time it was
named name here.
145 146 147 |
# File 'lib/hecks/ports/agent.rb', line 145 def suggest_name(registry, meaning:, kind:, near: []) Answers.suggestions(adapter(registry).suggest_name(meaning: meaning, kind: kind, near: near)) end |