Module: Hecks::Runtime

Defined in:
lib/hecks/runtime.rb,
lib/hecks/runtime/event.rb,
lib/hecks/runtime/value.rb,
lib/hecks/runtime/caller.rb,
lib/hecks/runtime/errors.rb,
lib/hecks/runtime/loader.rb,
lib/hecks/runtime/routing.rb,
lib/hecks/runtime/identity.rb,
lib/hecks/runtime/instance.rb,
lib/hecks/runtime/registry.rb,
lib/hecks/runtime/boot_gates.rb,
lib/hecks/runtime/dispatcher.rb,
lib/hecks/runtime/interpreting.rb,
lib/hecks/runtime/tenant_check.rb,
lib/hecks/runtime/tenant_scope.rb,
lib/hecks/runtime/command_rules.rb,
lib/hecks/runtime/rebuild_sweep.rb,
lib/hecks/runtime/reference_hop.rb,
lib/hecks/runtime/aggregate_lock.rb,
lib/hecks/runtime/entity_element.rb,
lib/hecks/runtime/value/coercion.rb,
lib/hecks/runtime/refusal_wording.rb,
lib/hecks/runtime/value/admission.rb,
lib/hecks/runtime/capability_graph.rb,
lib/hecks/runtime/saga_interpreter.rb,
lib/hecks/runtime/query_interpreter.rb,
lib/hecks/runtime/remote_dispatcher.rb,
lib/hecks/runtime/entity_interpreter.rb,
lib/hecks/runtime/policy_interpreter.rb,
lib/hecks/runtime/command_interpreter.rb,
lib/hecks/runtime/dependency_planning.rb,
lib/hecks/runtime/reaction_invocation.rb,
lib/hecks/runtime/registry/verification.rb,
lib/hecks/runtime/saga_pending_dispatch.rb,
lib/hecks/runtime/command_rules/emission.rb,
lib/hecks/runtime/read_model_interpreter.rb,
lib/hecks/runtime/command_rules/arithmetic.rb,
lib/hecks/runtime/command_rules/references.rb,
lib/hecks/runtime/registry/saga_persistence.rb,
lib/hecks/runtime/value/invariant_violation.rb,
lib/hecks/runtime/port_operation_interpreter.rb,
lib/hecks/runtime/command_rules/admissibility.rb,
lib/hecks/runtime/command_rules/authorization.rb,
lib/hecks/runtime/saga_interpreter/correlation.rb,
lib/hecks/ports/persistence/plugins/era/era_check.rb,
lib/hecks/ports/persistence/plugins/era/era_guard.rb,
lib/hecks/ports/persistence/plugins/era/era_tamper.rb,
lib/hecks/runtime/command_interpreter/argument_gate.rb,
lib/hecks/ports/persistence/plugins/era/storage_shape.rb,
lib/hecks/runtime/command_interpreter/mutation_applier.rb,
lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb

Defined Under Namespace

Modules: AggregateLock, Caller, DependencyPlanning, EntityElement, EraCheck, EraGuard, EraTamper, Identity, Interpreting, ReactionInvocation, RebuildSweep, ReferenceHop, RefusalWording, Routing, StorageShape, TenantCheck, TenantScope Classes: AbsentArgument, AlreadyExists, AttributeAbsent, BootGates, CapabilityGraph, CommandInterpreter, CommandRules, Dispatcher, EnsuresNotMet, EntityInterpreter, Event, GivenNotMet, Instance, InvariantViolation, LifecycleRefused, Loader, NotFound, NothingToCorrect, PolicyInterpreter, PortOperationInterpreter, ProjectionAbsent, QueryInterpreter, ReadModelInterpreter, Registry, RemoteDispatcher, RemoteRefusal, SagaInterpreter, StaleWrite, TypeMismatch, Unauthorized, UnknownArgument, UnknownVerb, Value, WiringError

Constant Summary collapse

DOMAIN_REFUSALS =

The domain saying NO — the errors a reaction may legitimately meet and record as an undelivered outcome. A policy whose target refuses is a fact about the domain ; the originating command still stands.

Everything ELSE is a defect : a NoMethodError in an interpreter, a NameError from a missing constant, a TypeError from a bad assumption. A blanket rescue StandardError used to fold both into one line — delivered: false, reason: "..." — so a crash in the runtime was indistinguishable from a rule doing its job, and read as normal operation in the log.

UnknownVerb IS one of these, and deliberately : a cross-domain policy (across "Notifications") fires in deployments where that domain is not loaded, and recording the undelivered reaction rather than raising is the design — spec/policy_spec states it in so many words, "records a reaction it cannot deliver rather than swallowing it". InvariantViolation belongs here and was missing. A value object refusing its own rule is the domain saying no as plainly as a given is — but the class is declared over in value.rb and never made the list, so the policy and saga interpreters, which rescue exactly these, would let it propagate as though the RUNTIME had broken. A reaction whose target violates an invariant is declined, not crashed. Found by spec/domain_refusal_spec on its first run : every corpus refusal must be a class named here, and 23 of banking's were InvariantViolation. THE NAMES COME FROM THE LANGUAGE, the classes from this module. DomainRefusal declares WHICH refusals are the domain's own — a rule the caller broke — as against a runtime fault. Resolving each name here means a refusal declared but never defined fails at load with a NameError, rather than being quietly absent from a list nothing re-checks.

Hecks::Vocabulary.fetch("DomainRefusal").map { |name| const_get(name) }.freeze
SAGA_PENDING_DISPATCH_KEY =

THE ONE SHARED CONSTANT between SagaInterpreter (the writer) and Registry::SagaPersistence#rehydrate_sagas! (the reader) for a scoped, minimal answer to the saga-durability review's item 8 (a durable outbox): a marker that survives exactly the window a crash in advance_saga/unwind can otherwise hide.

THE PROBLEM THIS CLOSES — checkpoint persists a saga's new state BEFORE the leg that justifies it (handler.dispatches) runs, and deliberately so: the mutex it holds is not reentrant, and a dispatch can re-enter this same interpreter. If the process dies in that window, the store says the transition happened and there is no record that its dispatch(es) never ran — not a refusal (the domain never got asked), not a defect (nothing raised), just silence indistinguishable from a leg that finished cleanly.

THE FIX — checkpoint now writes this key into the SAME already- durable memory blob (no new column, no adapter/schema change: memory is already an opaque, adapter-agnostic JSON blob every save_saga implementation round-trips verbatim) whenever it checkpoints a state a dispatch cascade hasn't run for YET, and clears it (a second checkpoint, pending: nil) once that cascade — success, refusal-compensated, defect-compensated, or ceiling- compensated — has actually run. A crash between those two writes leaves the marker standing; rehydrate_sagas! strips it back out of the LIVE instance's own :memory (so no dispatch/given/ fuzzer/doc consumer of a saga's memory ever sees this key — it exists only in the persisted blob) and surfaces it loudly instead.

WHAT THIS DELIBERATELY DOES NOT DO — auto-redrive the pending leg. Redelivering a dispatch whose outcome is genuinely unknown is only safe with idempotent delivery (the downstream command recognizing and no-op'ing a duplicate), which hecks's command/event pipeline has no mechanism for today. Blindly re-dispatching without that is how a stalled transfer becomes a DOUBLE-CREDITED one — a strictly worse defect than the stall it would replace. So this is real, durable, crash-surviving VISIBILITY into exactly what a stalled saga was doing when the process died — the missing half of "no reconciliation pass exists" — not the full pending → claimed → delivered outbox future-features.md still lists as unbuilt, and not a substitute for it.

:__hecks_saga_pending_dispatch__

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.current_registryObject (readonly)

The registry declarations are currently landing in, or nil outside a boot. Read by the DSL collectors on the top-level module and by the extraction port.



62
63
64
# File 'lib/hecks/runtime.rb', line 62

def current_registry
  @current_registry
end

Class Method Details

.as_caller(role:, actor_id: nil, as_of: nil, scope: nil, &block) ⇒ Object

Bind the ambient caller (see Runtime::Caller) for the duration of the block — who a command's declared role, if any, is checked against.

as_of: is OPTIONAL, same opt-in shape as actor_id: — a caller that wants a Governance RoleAssignment's starts_at enforced passes as_of: Ports::Clock.now(registry) here, at the door, exactly where cli_runner.rb already merges Clock.now into a command's own args. Nothing on the dispatch path calls the clock itself — see Ports::Clock's own header for why — so an unbound as_of leaves starts_at unchecked, exactly as before.

scope: is OPTIONAL too — a caller that states which scope it is acting in gets that scope checked against the matching RoleAssignment's own scope, not just its role_name. See Runtime::Caller::Current's own header for why this lives here rather than as a command-level DSL construct.



102
103
104
# File 'lib/hecks/runtime.rb', line 102

def as_caller(role:, actor_id: nil, as_of: nil, scope: nil, &block)
  Caller.as(role: role, actor_id: actor_id, as_of: as_of, scope: scope, &block)
end

.boot(path, shared: nil, install_facade: true, environment: nil) ⇒ Object

Load a bluebook directory and return the Dispatcher bound to it. install_facade:, environment: — see Loader.boot.



66
67
68
# File 'lib/hecks/runtime.rb', line 66

def boot(path, shared: nil, install_facade: true, environment: nil)
  Loader.boot(path, shared: shared, install_facade: install_facade, environment: environment)
end

.boot_files(paths, shared: nil, install_facade: true, environment: nil) ⇒ Object

paths form — see Loader.boot_files.



71
72
73
# File 'lib/hecks/runtime.rb', line 71

def boot_files(paths, shared: nil, install_facade: true, environment: nil)
  Loader.boot_files(paths, shared: shared, install_facade: install_facade, environment: environment)
end

.with_registry(registry) ⇒ Object

Bind the ambient registry for the duration of the block, restoring whatever was there before. Nesting is safe ; a raise still restores.



77
78
79
80
81
82
83
# File 'lib/hecks/runtime.rb', line 77

def with_registry(registry)
  previous          = @current_registry
  @current_registry = registry
  yield
ensure
  @current_registry = previous
end