hecks

DomainPort

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

A CLASS, not a module — the runtime instantiates whatever it resolves

for the port, so a bare module answers `NoMethodError: undefined

method ‘new’. Which itself arrives as a refuses` event rather than

an exception, exactly as documented under refuses 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

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

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

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 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 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 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

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" }