Words available inside port do ... end.
The tables on this page are generated from the language’s own
aggregate-local syntax tables (lib/hecks/language/**/*.bluebook)
by hecks language_run.project_reference — do not edit inside the markers. The prose
between them is hand-written and survives regeneration.
operation is the spelling the corpus uses; tells, asks and verb
appear nowhere in it, so this page declares a domain that carries all
four — a shipment that asks a carrier for a quote, is told when it was
delivered, and records itself through a swappable resource port:
```ruby bluebook Hecks.bluebook “DomainPortReference” do vision “A shipment, and the two directions a port can point.”
aggregate “Shipment” do attribute :waybill, Waybill
identified_by :waybill
attribute :note, Note
value_object("Waybill") { attribute :value, String }
value_object("Note") { attribute :text, String }
value_object("Position") { attribute :at, String }
command "Book" do
sets :waybill
sets :note
emits "ShipmentBooked"
end
query "Whereabouts" do
attribute :waybill, Waybill
returns Position
end end end ```
The adapter answering the ask refuses for one waybill and succeeds for the other, so both endings are reachable from a single boot:
```ruby boot
. Which itself arrives as a refuses` event rather thanrefuses below.class RefCarrier def quote(**args) raise “no service to that address” if args[:waybill].to_s.include?(“remote”)
{ "price" => { "cents" => 1_200 } } end
def whereabouts(**) { at: “the depot” } end end
Hecks::Adapters.const_set(:RefCarrier, RefCarrier) unless Hecks::Adapters.const_defined?(:RefCarrier, false) Hecks.adapter(“RefCarrier”) { port “Carrier” }
Hecks.hecksagon(“DomainPortReference”) do DomainPortReference::Shipment.persisted_by(“Memory”)
DomainPortReference::Shipment.port “Carrier” do asks “Quote” do attribute :waybill, Hecks::Bluebook::Reference.new(“Shipment”) answers “QuoteReturned” refuses “QuoteRefused” end
tells "Delivered" do
attribute :waybill, Hecks::Bluebook::Reference.new("Shipment")
emits "DeliveryReported"
end
answers_query "Whereabouts" end
# A PORT IS A verb OR ONE-OR-MORE operations, never both — so the
# resource port is a second port, not another word inside the first.
DomainPortReference::Shipment.port “Ledger” do
verb “recorded_by”
end
end
```ruby
runtime.dispatch("DomainPortReference::Shipment.Book", with: { waybill: { value: "wb-1" }, note: { text: "two crates" } })
runtime.dispatch("DomainPortReference::Shipment.Book", with: { waybill: { value: "wb-remote" }, note: { text: "one crate" } })
operation name, to: do ... end — opens a PortOperation body
| argument | kind | required | fills |
|—|—|—|—|
| positional 1 | text | true | name |
| to: | constant | false | to |
Opens one translation from an external fact into this domain’s own event vocabulary — reference_to, attribute, and emits, nothing else: the builder behind it defines no given or sets, so an operation cannot read aggregate state or mutate a record itself. A port declares operations or a verb (below) — never both, and never neither. Pizzas’ PaymentGateway port and its Receive operation (examples/pizzas/bluebook/pizzas.hecksagon) are the worked example — it only emits PizzaPaymentReceived; the actual rules (must have a topping, must still be available) stay on Order’s own Purchase command, reached through the OnPizzaPaymentReceived policy beside it. See the PortOperation reference page for the vocabulary inside.
An operation is addressed through its port, not directly off the
aggregate — Aggregate.Port.Operation:
shipment = runtime.registry.bluebook("DomainPortReference").aggregate("Shipment")
shipment.ports.map(&:name) # => ["Carrier"]
shipment.ports.find { |port| port.name == "Carrier" }.operations.map(&:hecks_name) # => ["Quote", "Delivered"]
tells name, to: do ... end — opens a PortOperation body
| argument | kind | required | fills |
|—|—|—|—|
| positional 1 | text | true | name |
| to: | constant | false | to |
What the outside TELLS this domain — an external fact arriving, translated
into the domain’s own word for it. Identical to operation, which is the
spelling every chapter in this corpus still uses and which keeps working;
tells is the same word under a name that says which way it points, now
that it has a twin.
It emits, and that is all. There is no channel back to whoever called: an
inbound operation is the anti-corruption boundary, and whatever should
happen next happens wherever a policy reacts to the event it emitted.
Declaring answers or refuses on one is refused when the bluebook builds.
Delivered is an inbound fact: something outside says the crate
arrived, and the domain records that it was told:
told = runtime.dispatch("DomainPortReference::Shipment.Carrier.Delivered", to: "wb-1", with: { waybill: "wb-1" })
told.events.map(&:name) # => ["DeliveryReported"]
Written exemption (ADR 0025 principle 4) — no real corpus member
declares tells; pizzas’ PaymentGateway.Receive (the one real
inbound port operation this corpus has) still spells it operation.
The MECHANISM tells names is proven for real by that call — the two
words fill the same PortOperation construct, identically — what is
unproven is only the SPELLING, and giving tells its own separate
corpus member would mean inventing a second inbound integration this
codebase does not otherwise need, for a word that changes nothing
about how the runtime behaves once declared.
Nothing goes back. The event is the whole of it, and what happens next
is a policy’s business:
carrier = shipment.ports.find { |port| port.name == "Carrier" }
delivered = carrier.operations.find { |operation| operation.hecks_name == "Delivered" }
delivered.answers # => nil
asks name, to: do ... end — opens a PortOperation body
| argument | kind | required | fills |
|—|—|—|—|
| positional 1 | text | true | name |
| to: | constant | false | to |
What this domain ASKS of the outside — the direction the language did not
have until it was added, and the reason a port can now be read in both
directions.
Before it, a domain could be called by an adapter and never call one:
Ports::Extraction.adapter.canonical(...) is library code reaching for an
adapter, and MockStripeAdapter#create_session is an application doing the
same. Neither is the domain asking, and neither leaves a trace in the record.
An asks is dispatched like any other port operation — a policy reaches it
off an event, because the dispatcher resolves ports before entities — and it
comes back as one of the two events it named. That is what makes the outside
world something the model can reason about instead of a place exceptions come
from.
A policy reaches an asks by name: ask :quote in the bluebook names the need,
and boot matches it to the declared ask by the event’s aggregate and the name,
so the bluebook never spells the port (see ask in the policy reference). When
one aggregate declares the same ask name on two ports, the hecksagon picks with
Domain::Aggregate.ask_via "Quote", port: "Carrier"; boot refuses the ambiguity
until it does. A policy that still writes trigger Aggregate::Port::Operation
keeps working, and model_check warns about it.
It must name both endings (answers and refuses) and may not emits.
An ask that named only its happy ending would put the failure somewhere the
model cannot see, which is the whole reason a boundary is worth modelling.
The adapter really is called, and what it returned comes back as the event the ask named:
answered = runtime.dispatch("DomainPortReference::Shipment.Carrier.Quote", to: "wb-1", with: { waybill: "wb-1" })
answered.events.map(&:name) # => ["QuoteReturned"]
And when the outside says no, that is an event too — not an exception escaping into the caller:
refused = runtime.dispatch("DomainPortReference::Shipment.Carrier.Quote", to: "wb-remote", with: { waybill: "wb-remote" })
refused.events.map(&:name) # => ["QuoteRefused"]
verb verb — fills verb
| argument | kind | required | fills | |—|—|—|—| | positional 1 | text | true | verb |
The driven half of the same port call — a swappable resource port
(persisted_by, opened_by, a project’s own provided_by) rather than a
translation of an inbound fact. verb "opened_by" registers the exact same
IR::Port a standalone .port file’s Hecks.port "name" do verb "..."
end would, reached by whichever adapters declare port "Checkout" and
bound the same way — one line, next to the aggregate it belongs to,
instead of a separate file. A port is a verb or one-or-more operations,
never both; declaring neither, or both, refuses to build. See
writing-an-adapter.md’s own section on resource ports for a worked
example.
A verb port declares no operations at all — it names the binding word
an adapter is reached by:
A verb port does not land beside the driving ports on the aggregate —
it registers as a RESOURCE port on the registry itself, next to the
persistence and extraction ports the framework declares the same way:
runtime.registry.ports.keys.sort # => ["Ledger", "extraction", "persistence"]
Which is the difference the prose above is drawing. A driving port is part of the domain’s own surface; a resource port is a socket an adapter plugs into, and it has no operations of its own to address.
Written exemption (ADR 0025 principle 4) — every resource port a
real domain here actually needs (persisted_by, projected_by,
opened_by) is a FRAMEWORK-level default (hecksagon_builder.rb’s
own settings words), never a project declaring its own port "X" do
verb "..." end line — so nothing in examples/ or
lib/hecks/framework/ exercises the bare construct this section
documents. writing-an-adapter.md’s own worked example is the closest
this repo has to a real one, and it is a guide, not a corpus member.
signal signal — fills signal
| argument | kind | required | fills | |—|—|—|—| | positional 1 | symbol | true | signal |
Whether calling this resource port answers with a value (:reply) or fires an effect with nothing to hand back (:effect) — the SAME closed set (and the same default when never declared) port.md’s own standalone .port file signal already carries; a bare-verb DomainPort is registered as the exact same IR::Port, so it means the identical thing here. Only meaningful alongside verb — an operation-shaped port has no single signal of its own, since each operation answers (or doesn’t) independently.
"Ledger" never called signal at all, so it defaults:
runtime.registry.ports["Ledger"].signal # => :reply
answers answers — fills answers
| argument | kind | required | fills | |—|—|—|—| | positional 1 | symbol | true | answers |
port.md’s own answers — reachable here too because a bare-verb DomainPort is registered as the exact same IR::Port a standalone .port file builds (this page’s own signal section, just above, says why). Only meaningful alongside verb, same as signal; an operation-shaped port has nothing to respond_to? here — each operation answers on its own. extraction.port’s own real declaration is answers :canonical, migrated to build through this same DomainPort entry point.
runtime.registry.ports["extraction"].answers # => [:canonical]
answers_query name — fills answered_queries
| argument | kind | required | fills | |—|—|—|—| | positional 1 | text | true | name |
Binds one of the aggregate’s own queries to this port’s adapter, so the answer comes from
outside the domain instead of from stored records. The bluebook owns the meaning: the query
declares the question and the shape of its answer, returns Position, a value object of the
aggregate (returns list_of(Position) for many rows). It never names a port or an adapter; the
hecksagon does, here, and says nothing else. The hecksagon owns the capability (who can obtain the
answer), the world owns the configuration.
The adapter bound to the port is asked by the query’s snake-cased name, with the query’s
arguments as plain data. Each row it answers is built as the returned value object, so an answer
that is not that shape (a missing field, a wrong type, a broken invariant, an extra key) is
refused with a TypeMismatch, InvariantViolation or UnknownArgument that names the query,
before it enters the domain. The aggregate is never read and no event is written. The answer is
not in the journal, so it cannot be replayed; a query that wants to say when it was taken declares
attribute :taken_at, String in its value object and the adapter fills it from the clock port.
A query has exactly one answer path. It is derived from the aggregate’s records when it declares
a where, order_by, limit or offset (or takes no arguments and lists every record) and
returns nothing; it is answered from outside when it returns a value object and is bound here.
Booting refuses a query with no path (a returns nothing binds, or arguments that select
nothing), with two (bound twice, or bound and also filtering records), a binding that names a
query the aggregate does not declare, and a bound query whose port has no adapter, two, or one
that lacks the method.
A behaviors file swaps the adapter without touching the bluebook: it loads a sibling
adapters/ folder whose adapter implements the same port with canned answers.
where = runtime.query("DomainPortReference::Shipment.Whereabouts", waybill: { value: "wb-1" }).first
where # => { at: "the depot" }