Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

emit from any rule mode

emit publishes a typed value to a sink — a named, write-only egress channel. It is the language’s hook into the system’s pub/sub layer: a published value flows to whatever external observers a sink is wired to, and nothing else happens to the knowledge graph. An emit adds no axiom, derives no tuple, and can never change the queryable extent — it is an observation, not a mutation. Because it is extent-neutral, emit is admitted in every rule mode (fn, query, derive, mutate, check), not only the side-effecting one; a fn that emits is still pure in the sense the purity ladder cares about — its return value and the extent are unchanged.

Sinks

A sink is the typed publication channel emit targets. It names a channel and the type of value that channel accepts; it carries no body and no target:

sink-decl ::= attribute* 'pub'? 'sink' Ident ':' TypeExpr ';'
pub sink AuditLog: LeaseEvent;
pub sink HitlQueue: ReviewTask;
pub sink Notify:    Notification;

There is exactly one sink form. A brace-bodied form pub sink Name { … } is not a sink declaration (a sink has no in-language body — where a value is delivered is configuration, not source); it is refused with OE1356. Delivery — log file, webhook, message queue, HITL ticket — is a runtime concern configured outside the source (the egress dual of the read-side placement routing); the language guarantees only that an emitted value matches the sink’s declared type.

Two spellings, one mechanism

emit means the same thing everywhere — publish value v to sink S — but the rule modes come in two body shapes, so it has two surface spellings.

Imperative bodies (mutate, fn block) have a statement position, so emit is a statement:

emit-stmt ::= 'emit' sink-path '{' expr '}' ';'
sink-path ::= Ident ('::' Ident)*
pub type Person { name: String }
pub sink AuditLog: String;

pub mutate sign_lease(t: Person) {
    // … the write …
    emit AuditLog { t.name };
}

Declarative bodies (derive, query, check) are clause bodies (select … from …, head :- atoms) with no statement position, so emit rides a consequence arrow => — the same arrow check already uses. In fact check’s => Diagnostic { … } is an emit to the reserved Diagnostics sink; it is the Diagnostics-channel special case of the general consequence form:

pub type Person { age: Int }
pub sink SeniorFeed: Person;

// derive: publish on each newly-derived tuple (the signed derivation delta)
pub derive senior(p: Person) :- p: Person, p.age >= 65 => emit SeniorFeed { p };

A check’s => Diagnostic { … } payload is the same construct: it is sugar for => emit Diagnostics { Diagnostic { … } }, an emit to the reserved typed sink Diagnostics: Diagnostic (check). emit Diagnostics { … } is otherwise reserved — the diagnostic stream is produced only through check, never by a hand-written emit. As a check consequence it carries polarity like any other derive-class sink: a new violation emits an Asserted diagnostic and a resolved violation emits a Retracted one (the diagnostic clears).

Firing — once per fresh delta, with polarity

A sink is fed once per fresh firing, where the firing unit is the natural one for the mode. Re-deriving, a cache hit, or re-reading an unchanged result does not re-emit. Each firing carries a polarity: Asserted (a value newly published) or Retracted (a previously-published delta withdrawn).

ModeSpellingFires once per…Polarity
mutateemit S { e };committed write, in statement order — nothing is emitted on abort (a failed require publishes nothing, consistent with the all-or-nothing transaction)Asserted
fnemit S { e };fresh evaluation; a cache hit on (args, state_version) does not re-emitAsserted
query=> emit S { e }fresh evaluationAsserted
derive=> emit S { e }head tuple crossing a truth boundaryAsserted on newly-true (+1 delta), Retracted on newly-false (−1 delta)
check=> emit S { e } / => Diagnostic { … }violation binding crossing a truth boundaryAsserted on a new violation, Retracted when one is resolved

So mutate/fn/query fire per execution/evaluation and derive/check fire per binding — but in every case the rule is “fire on each fresh occurrence, deduplicated by the cache/delta,” which is why the published stream tracks the system’s evolution rather than how often something is asked.

The delta model. The two delta-firing modes carry both signs. A derive sink sees Asserted on the +1 delta when a head tuple becomes true and Retracted on the −1 delta when a previously-true tuple becomes false; a re-derivation that nets to zero emits nothing. A check sink sees Asserted on a new violation binding and Retracted when a previously-firing violation is resolved. Emitting only the positive delta and dropping retractions would be a silent egress lie: a consumer would learn that an entitlement was granted but never that it was revoked. Polarity closes that — the egress stream is faithful to the extent’s evolution in both directions.

The imperative and pull modes are Asserted-only by construction, not by omission. A mutate emits per committed write and a committed write is never un-happened — an abort emits nothing rather than a later Retracted. A fn emits per fresh evaluation, and query is pull-based: a pull produces a result, never a standing delta there is anything to withdraw. There is no retraction to carry because these modes have no negative delta, so they are complete.

Emission and delivery

An emission is a triple { sink, value, polarity }. The value is the evaluated typed value — the same value carrier the language uses for struct and enum values, so a sink’s payload type is an ordinary type and the emitted value is checked against it. The polarity is the firing sign above.

Emissions ride the commit’s observation channel — the same receipt channel that carries #[observe] and Warning / Info check diagnostics — not the axiom/event log of facts (Storage layer). This is why emit is model-neutral: an emission is never an axiom, so it cannot enter the well-founded model, and its durability does not change the queryable extent.

Delivery is at-least-once with idempotency — effectively-once effect, not effectively-once delivery, which is unattainable across an independent process boundary (the two-generals impossibility). The mechanism is a transactional outbox over the append-only commit log (Richardson, Microservices Patterns; the partitioned-store reasoning of Helland, “Life beyond Distributed Transactions”): an emission is recorded atomically with the commit that produced it, then delivered; a redelivery after a crash or timeout is deduplicated downstream by an idempotency key derived from the emission and its originating commit. A consumer that applies emissions idempotently observes each effect once.

Routing is configuration. Which physical destination a sink’s values reach — a log file, a webhook, a message queue, a human-in-the-loop ticket — is runtime configuration (ox.toml), the egress dual of the read-side [placement] routing. It never appears in source: a sink declaration names the channel and its payload type and nothing more, and emit guarantees only that the value matches that type.

Status

emit is settled surface and executes in all five rule modes. The mutate and fn statement forms publish per fresh execution/evaluation — Asserted, nothing on abort, and a fn cache hit on (args, state) does not re-emit. The derive => emit consequence publishes the signed derivation delta at the commit boundary: Asserted on a newly-true head tuple, Retracted when a previously-true one becomes false, nothing on a net-zero re-derivation. The check => emit consequence publishes from the violation delta (Asserted on a new violation, Retracted when one resolves). The query => emit consequence publishes one Asserted value per fresh evaluation (a cache hit does not re-emit). Every position type-checks the value against the sink (OE0201 on a mismatch), refuses an undeclared sink (OE0101), and refuses the reserved Diagnostics sink to a hand-written emit (OE1368 — the diagnostic stream is produced only through check’s => Diagnostic { … }). An emit-bearing fn inlined into a rule body is refused (OE0227 — a rule body has no egress surface). A check => emit that fires at ox check / ox build has no receipt at build, so it surfaces a non-blocking OW1369 (its egress is a runtime-only effect, never silently dropped). Egress is model-neutral throughout: it asserts no axiom and never changes the queryable extent — emissions ride the commit’s observation channel (mutate / derive / check on the mutation receipt) or the read’s dispatch response (query), never the axiom log. Delivery and routing (log file, webhook, message queue, HITL ticket) are runtime configuration — the egress dual of read-side placement. The one sink-declaration form is pub sink Name: Type;; a brace-bodied pub sink Name { … } is refused by name (OE1356) — a sink has no body.