hecks 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/hecks/adapters/driven/claude_code.rb +65 -0
- data/lib/hecks/adapters/driven/folder.rb +73 -0
- data/lib/hecks/adapters/driven/google_authentication.rb +25 -4
- data/lib/hecks/adapters/driven/governance_authorization.rb +57 -6
- data/lib/hecks/adapters/driven/heki/journal.rb +8 -0
- data/lib/hecks/adapters/driven/heki/saga_store.rb +53 -7
- data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
- data/lib/hecks/adapters/driven/heki.rb +93 -2
- data/lib/hecks/adapters/driven/identity_registry.rb +10 -0
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +22 -0
- data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
- data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
- data/lib/hecks/adapters/driven/lambda/client.rb +35 -7
- data/lib/hecks/adapters/driven/lambda.rb +43 -2
- data/lib/hecks/adapters/driven/local_storage.rb +67 -1
- data/lib/hecks/adapters/driven/memory.rb +13 -13
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +20 -0
- data/lib/hecks/adapters/driven/prism.rb +46 -7
- data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
- data/lib/hecks/adapters/driven/system_clock.rb +3 -0
- data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
- data/lib/hecks/adapters/driven.rb +2 -0
- data/lib/hecks/adapters/driving/github_webhook.rb +14 -1
- data/lib/hecks/behaviors/dsl.rb +58 -0
- data/lib/hecks/behaviors/expectations.rb +168 -7
- data/lib/hecks/behaviors/ir.rb +11 -0
- data/lib/hecks/behaviors/rspec.rb +8 -0
- data/lib/hecks/behaviors/runner.rb +19 -0
- data/lib/hecks/behaviors.rb +8 -0
- data/lib/hecks/bluebook/aggregate.rb +30 -0
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +7 -0
- data/lib/hecks/bluebook/assembly/build.rb +25 -4
- data/lib/hecks/bluebook/assembly/contract.rb +66 -14
- data/lib/hecks/bluebook/assembly/contracts.rb +24 -19
- data/lib/hecks/bluebook/assembly/marks.rb +143 -14
- data/lib/hecks/bluebook/assembly/specializer.rb +19 -0
- data/lib/hecks/bluebook/assembly.rb +25 -7
- data/lib/hecks/bluebook/attribute.rb +17 -3
- data/lib/hecks/bluebook/behaviour/aggregate.rb +32 -0
- data/lib/hecks/bluebook/behaviour/attribute.rb +13 -0
- data/lib/hecks/bluebook/behaviour/chapter.rb +55 -2
- data/lib/hecks/bluebook/behaviour/command.rb +33 -3
- data/lib/hecks/bluebook/behaviour/domain_port.rb +20 -0
- data/lib/hecks/bluebook/behaviour/entity.rb +12 -0
- data/lib/hecks/bluebook/behaviour/hexagon.rb +26 -0
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +23 -2
- data/lib/hecks/bluebook/behaviour/policy.rb +34 -9
- data/lib/hecks/bluebook/behaviour/process_manager.rb +32 -1
- data/lib/hecks/bluebook/behaviour/query.rb +5 -0
- data/lib/hecks/bluebook/behaviour/read_model.rb +21 -0
- data/lib/hecks/bluebook/behaviour/traits.rb +36 -0
- data/lib/hecks/bluebook/behaviour/value_object.rb +17 -5
- data/lib/hecks/bluebook/chapter.rb +23 -0
- data/lib/hecks/bluebook/command.rb +53 -8
- data/lib/hecks/bluebook/domain_port.rb +25 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +196 -37
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +79 -42
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +59 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +385 -53
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +135 -21
- data/lib/hecks/bluebook/dsl/command_builder.rb +189 -27
- data/lib/hecks/bluebook/dsl/entity_builder.rb +139 -9
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +41 -0
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +23 -6
- data/lib/hecks/bluebook/dsl/policy_builder.rb +60 -19
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +79 -15
- data/lib/hecks/bluebook/dsl/query_builder.rb +33 -4
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +102 -27
- data/lib/hecks/bluebook/dsl/rule_reference.rb +60 -8
- data/lib/hecks/bluebook/dsl/translation_builder.rb +146 -40
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +55 -7
- data/lib/hecks/bluebook/entity.rb +29 -0
- data/lib/hecks/bluebook/expression/ast_json.rb +115 -23
- data/lib/hecks/bluebook/expression/ast_reader.rb +29 -0
- data/lib/hecks/bluebook/expression/canonical_form.rb +48 -9
- data/lib/hecks/bluebook/expression/evaluator.rb +207 -29
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +36 -0
- data/lib/hecks/bluebook/expression/resolver.rb +320 -77
- data/lib/hecks/bluebook/hexagon.rb +34 -0
- data/lib/hecks/bluebook/lifecycle.rb +11 -0
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -0
- data/lib/hecks/bluebook/meta_validator/judge.rb +30 -26
- data/lib/hecks/bluebook/meta_validator/plan.rb +43 -8
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +1 -0
- data/lib/hecks/bluebook/meta_validator/readings.rb +157 -7
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +24 -4
- data/lib/hecks/bluebook/meta_validator/shapes.rb +141 -0
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +149 -23
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +5 -4
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +1 -0
- data/lib/hecks/bluebook/meta_validator.rb +180 -84
- data/lib/hecks/bluebook/model_check.rb +268 -24
- data/lib/hecks/bluebook/pattern_subset.rb +23 -1
- data/lib/hecks/bluebook/process_manager.rb +13 -0
- data/lib/hecks/bluebook/project_discovery.rb +5 -0
- data/lib/hecks/bluebook/project_loader.rb +40 -0
- data/lib/hecks/bluebook/project_register.rb +44 -0
- data/lib/hecks/bluebook/query.rb +27 -0
- data/lib/hecks/bluebook/read_model.rb +21 -1
- data/lib/hecks/bluebook/reference.rb +21 -8
- data/lib/hecks/bluebook/smoke_test.rb +29 -6
- data/lib/hecks/bluebook/synthesizer.rb +34 -0
- data/lib/hecks/bluebook/translation.rb +30 -1
- data/lib/hecks/bluebook/value_object.rb +23 -5
- data/lib/hecks/bluebook.rb +3 -4
- data/lib/hecks/codemod.rb +107 -20
- data/lib/hecks/construct.rb +15 -1
- data/lib/hecks/corpus.rb +146 -25
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
- data/lib/hecks/doc/reference.rb +185 -16
- data/lib/hecks/embryonaut_bluebook.rb +32 -9
- data/lib/hecks/facade/handle.rb +76 -3
- data/lib/hecks/facade/surface/aggregate_door.rb +8 -0
- data/lib/hecks/forms/field_shape.rb +3 -0
- data/lib/hecks/forms/page.rb +14 -0
- data/lib/hecks/forms/port_argument.rb +12 -0
- data/lib/hecks/forms/query_form_renderer.rb +63 -0
- data/lib/hecks/forms/record_renderer.rb +58 -0
- data/lib/hecks/forms/record_table.rb +27 -0
- data/lib/hecks/forms/reference_options.rb +24 -0
- data/lib/hecks/forms/value_object_shape.rb +10 -0
- data/lib/hecks/fqn.rb +58 -0
- data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
- data/lib/hecks/framework/oidc.json +15 -0
- data/lib/hecks/framework.rb +43 -20
- data/lib/hecks/freezer.rb +17 -1
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +159 -24
- data/lib/hecks/fuzzing/combination_miner.rb +59 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +109 -8
- data/lib/hecks/fuzzing/coverage_campaign.rb +56 -13
- data/lib/hecks/fuzzing/differential.rb +34 -0
- data/lib/hecks/fuzzing/domain_generator.rb +188 -11
- data/lib/hecks/fuzzing/era_boundary.rb +45 -15
- data/lib/hecks/fuzzing/form_census.rb +86 -0
- data/lib/hecks/fuzzing/generated_domain_check.rb +76 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +39 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +79 -22
- data/lib/hecks/fuzzing/nondeterministic.rb +13 -1
- data/lib/hecks/fuzzing/persistence_parity.rb +95 -3
- data/lib/hecks/fuzzing/properties/corrections.rb +25 -0
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +158 -14
- data/lib/hecks/fuzzing/properties/guards.rb +44 -0
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +48 -0
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +18 -0
- data/lib/hecks/fuzzing/properties/outbox.rb +49 -11
- data/lib/hecks/fuzzing/properties/querying.rb +68 -14
- data/lib/hecks/fuzzing/properties.rb +24 -15
- data/lib/hecks/fuzzing/qa_settings.rb +12 -0
- data/lib/hecks/fuzzing/replay.rb +137 -29
- data/lib/hecks/fuzzing/rotation_priority.rb +41 -21
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +46 -20
- data/lib/hecks/fuzzing/self_consistency.rb +189 -40
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +12 -6
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +1 -1
- data/lib/hecks/fuzzing/sequence_generator.rb +47 -0
- data/lib/hecks/fuzzing/shrinker.rb +114 -2
- data/lib/hecks/fuzzing/structural_skips.rb +18 -4
- data/lib/hecks/fuzzing/sweep_depth.rb +8 -0
- data/lib/hecks/fuzzing/target_capabilities.rb +61 -14
- data/lib/hecks/fuzzing/value_generator.rb +98 -10
- data/lib/hecks/grammar/evolve.rb +178 -2
- data/lib/hecks/grammar.rb +46 -0
- data/lib/hecks/ir.rb +38 -7
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
- data/lib/hecks/literal.rb +32 -0
- data/lib/hecks/naming.rb +88 -7
- data/lib/hecks/ports/access_control.rb +5 -10
- data/lib/hecks/ports/authorization.rb +3 -6
- data/lib/hecks/ports/identity_assignment.rb +1 -2
- data/lib/hecks/ports/identity_resolution.rb +1 -2
- data/lib/hecks/ports/key_vault.port +6 -0
- data/lib/hecks/ports/key_vault.rb +58 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +6 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +38 -2
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +40 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +92 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +8 -0
- data/lib/hecks/ports/query/in_memory.rb +39 -6
- data/lib/hecks/ports/query/ordering.rb +15 -0
- data/lib/hecks/ports.rb +1 -0
- data/lib/hecks/projections/bootstrap_table.rb +43 -8
- data/lib/hecks/projections/diagrams.rb +243 -7
- data/lib/hecks/projections/glossary/html.rb +88 -0
- data/lib/hecks/projections/glossary/markdown.rb +47 -0
- data/lib/hecks/projections/glossary/mermaid.rb +48 -0
- data/lib/hecks/projections/glossary/sentences.rb +105 -0
- data/lib/hecks/projections/glossary.rb +161 -19
- data/lib/hecks/projections/model/deviations.rb +44 -0
- data/lib/hecks/projections/model.rb +51 -1
- data/lib/hecks/projections/oidc.rb +40 -3
- data/lib/hecks/projections/parser_table.rb +49 -8
- data/lib/hecks/projections/reference.rb +12 -0
- data/lib/hecks/projections/rust_vocabulary.rb +219 -16
- data/lib/hecks/projections/shape.rb +8 -0
- data/lib/hecks/projections/statements.rb +63 -16
- data/lib/hecks/projections/vocabulary.rb +17 -0
- data/lib/hecks/projector/cli_projector.rb +218 -10
- data/lib/hecks/projector/docs_projector.rb +145 -19
- data/lib/hecks/projector/exporter.rb +65 -11
- data/lib/hecks/projector/ir_projector.rb +6 -0
- data/lib/hecks/projector/narrate_projector.rb +136 -15
- data/lib/hecks/projector/target.rb +29 -5
- data/lib/hecks/projector.rb +74 -6
- data/lib/hecks/query_ir.rb +47 -0
- data/lib/hecks/query_specification/common/null_policy.rb +5 -3
- data/lib/hecks/rendering.rb +6 -0
- data/lib/hecks/router/namespace_installer.rb +13 -0
- data/lib/hecks/router.rb +55 -0
- data/lib/hecks/runtime/aggregate_lock.rb +9 -0
- data/lib/hecks/runtime/boot_gates.rb +18 -0
- data/lib/hecks/runtime/caller.rb +32 -0
- data/lib/hecks/runtime/capability_graph.rb +11 -0
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +23 -21
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +14 -15
- data/lib/hecks/runtime/command_interpreter.rb +42 -17
- data/lib/hecks/runtime/command_rules/admissibility.rb +165 -14
- data/lib/hecks/runtime/command_rules/arithmetic.rb +17 -5
- data/lib/hecks/runtime/command_rules/references.rb +118 -28
- data/lib/hecks/runtime/dependency_planning.rb +45 -0
- data/lib/hecks/runtime/dispatcher.rb +28 -50
- data/lib/hecks/runtime/entity_element.rb +161 -8
- data/lib/hecks/runtime/entity_interpreter.rb +44 -9
- data/lib/hecks/runtime/errors.rb +18 -4
- data/lib/hecks/runtime/event.rb +10 -5
- data/lib/hecks/runtime/identity.rb +71 -3
- data/lib/hecks/runtime/instance.rb +67 -7
- data/lib/hecks/runtime/interpreting.rb +13 -5
- data/lib/hecks/runtime/invocation.rb +118 -36
- data/lib/hecks/runtime/loader.rb +94 -8
- data/lib/hecks/runtime/outbox.rb +145 -7
- data/lib/hecks/runtime/policy_interpreter.rb +22 -9
- data/lib/hecks/runtime/port_operation_interpreter.rb +20 -0
- data/lib/hecks/runtime/query_interpreter.rb +40 -12
- data/lib/hecks/runtime/reaction_invocation.rb +53 -8
- data/lib/hecks/runtime/read_model_interpreter.rb +23 -7
- data/lib/hecks/runtime/rebuild_sweep.rb +28 -0
- data/lib/hecks/runtime/reference_hop.rb +42 -0
- data/lib/hecks/runtime/refusal_wording.rb +50 -0
- data/lib/hecks/runtime/registry/saga_persistence.rb +11 -0
- data/lib/hecks/runtime/registry/verification.rb +119 -4
- data/lib/hecks/runtime/registry.rb +157 -4
- data/lib/hecks/runtime/remote_dispatcher.rb +92 -6
- data/lib/hecks/runtime/routing.rb +27 -2
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +10 -12
- data/lib/hecks/runtime/saga_interpreter.rb +27 -13
- data/lib/hecks/runtime/tenant_check.rb +26 -6
- data/lib/hecks/runtime/tenant_scope.rb +18 -0
- data/lib/hecks/runtime/value/coercion.rb +255 -33
- data/lib/hecks/runtime/value/entity_list_coercion.rb +102 -30
- data/lib/hecks/runtime/value.rb +50 -0
- data/lib/hecks/runtime.rb +32 -0
- data/lib/hecks/storehouse.rb +305 -9
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
- data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks.rb +79 -1
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -0
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +12 -2
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +29 -5
- metadata +11 -5
- data/lib/hecks/codemod/legacy_dispatch_args.rb +0 -299
- data/lib/hecks/codemod/legacy_dispatch_recorder.rb +0 -186
- data/lib/hecks/deprecation.rb +0 -95
data/lib/hecks/storehouse.rb
CHANGED
|
@@ -19,9 +19,11 @@ module Hecks
|
|
|
19
19
|
# stdio — not the whole thing; a plain CLI door, an HTTP door, a second
|
|
20
20
|
# transport of any shape, would sit beside it on the exact same bus,
|
|
21
21
|
# sharing the same audit log and caller-identity handling, without
|
|
22
|
-
# ever needing to speak MCP. (This
|
|
23
|
-
#
|
|
24
|
-
#
|
|
22
|
+
# ever needing to speak MCP. (This bus is deliberately not
|
|
23
|
+
# `Facade::McpDoor` — folding it into `Facade` under that name would
|
|
24
|
+
# conflate the bus with its one built transport.)
|
|
25
|
+
#
|
|
26
|
+
# ## The surface
|
|
25
27
|
#
|
|
26
28
|
# catalog — what aggregates a domain declares, and what each can do
|
|
27
29
|
# describe — one aggregate's full command/query/refusal contract
|
|
@@ -37,6 +39,8 @@ module Hecks
|
|
|
37
39
|
# behaviors — run a domain's hand-curated `.behaviors` examples
|
|
38
40
|
# follow — tail this bus's own dispatch/query/state audit log
|
|
39
41
|
#
|
|
42
|
+
# ## Auditing and caller identity
|
|
43
|
+
#
|
|
40
44
|
# `dispatch`/`query`/`state` each take a `summary` — the survey's "every
|
|
41
45
|
# audit row carries human intent for free" — and `dispatch`/`query`
|
|
42
46
|
# additionally take an optional `source:` (`SOURCE_TAGS`, the survey's
|
|
@@ -58,6 +62,8 @@ module Hecks
|
|
|
58
62
|
# `describe`/`validate`/`domains`/`history`/`behaviors`/`events` need
|
|
59
63
|
# neither — they change nothing and commit nothing to any log.
|
|
60
64
|
#
|
|
65
|
+
# ## Booting is the caller's job
|
|
66
|
+
#
|
|
61
67
|
# A caller hands in an already-booted `runtime`, the same division of
|
|
62
68
|
# labor `Facade::CliRunner` already keeps against `bin/run`: booting a
|
|
63
69
|
# domain from a path is IO the calling `bin/` script owns, this stays a
|
|
@@ -68,6 +74,8 @@ module Hecks
|
|
|
68
74
|
# takes the domain path instead and boots it; `domains` has no
|
|
69
75
|
# domain to be handed one of yet, that's what it's answering.
|
|
70
76
|
#
|
|
77
|
+
# ## Built from existing doors
|
|
78
|
+
#
|
|
71
79
|
# No new vocabulary otherwise. Every method here composes doors that
|
|
72
80
|
# already exist — `Projector.call(:cli, ...)` for verb/question alias
|
|
73
81
|
# resolution (the identical table `CliRunner` itself resolves against),
|
|
@@ -108,11 +116,19 @@ module Hecks
|
|
|
108
116
|
# project tree.
|
|
109
117
|
BOOT_ROOT = File.expand_path(ENV["HECKS_STOREHOUSE_ROOT"] || File.expand_path("../..", __dir__))
|
|
110
118
|
|
|
119
|
+
# Resolves `path` against `BOOT_ROOT` and refuses one that escapes it.
|
|
120
|
+
#
|
|
111
121
|
# **Refused, not silently clamped** — a path outside `BOOT_ROOT` is either
|
|
112
122
|
# an honest mistake (a relative path typed against the wrong cwd) or
|
|
113
123
|
# the exact thing this check exists to catch, and both deserve the
|
|
114
124
|
# same clear refusal rather than a silent rewrite to something the
|
|
115
125
|
# caller didn't ask for.
|
|
126
|
+
#
|
|
127
|
+
# @param path [String, #to_s] the caller-supplied path to confine
|
|
128
|
+
# @param label [String] the argument's own name, for the refusal message
|
|
129
|
+
# (e.g. `"domain"`, `"under"`)
|
|
130
|
+
# @return [String] `path` resolved to an absolute path under `BOOT_ROOT`
|
|
131
|
+
# @raise [Runtime::TypeMismatch] if the resolved path is outside `BOOT_ROOT`
|
|
116
132
|
def confine!(path, label)
|
|
117
133
|
resolved = File.expand_path(path.to_s, BOOT_ROOT)
|
|
118
134
|
return resolved if resolved == BOOT_ROOT || resolved.start_with?("#{BOOT_ROOT}#{File::SEPARATOR}")
|
|
@@ -128,11 +144,22 @@ module Hecks
|
|
|
128
144
|
# projects a whole-domain CLI or doc set makes this same assumption
|
|
129
145
|
# (`Facade::CliRunner#call`'s own `bluebook = runtime.registry.
|
|
130
146
|
# bluebooks.values.first`) — one `.hecksagon` names one chapter.
|
|
147
|
+
# The one bluebook this runtime booted.
|
|
148
|
+
#
|
|
149
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
|
|
150
|
+
# @return [Bluebook::Chapter] the runtime's one loaded bluebook
|
|
151
|
+
# @raise [Runtime::NotFound] if the runtime's boot loaded no bluebook
|
|
131
152
|
def bluebook_for(runtime)
|
|
132
153
|
runtime.registry.bluebooks.values.first or
|
|
133
154
|
raise Runtime::NotFound, "this boot loaded no bluebook"
|
|
134
155
|
end
|
|
135
156
|
|
|
157
|
+
# Finds one aggregate by name on a bluebook, or refuses.
|
|
158
|
+
#
|
|
159
|
+
# @param bluebook [Bluebook::Chapter] the chapter to search
|
|
160
|
+
# @param name [String, Symbol, #to_s] the aggregate's declared name
|
|
161
|
+
# @return [Bluebook::Aggregate] the matching aggregate
|
|
162
|
+
# @raise [Runtime::NotFound] if `bluebook` declares no aggregate named `name`
|
|
136
163
|
def aggregate_ir!(bluebook, name)
|
|
137
164
|
bluebook.aggregate(name) or
|
|
138
165
|
raise Runtime::NotFound, "#{bluebook.name} declares no aggregate named #{name.inspect} — " \
|
|
@@ -143,6 +170,13 @@ module Hecks
|
|
|
143
170
|
# short name when it's unambiguous, the qualified `Aggregate.Verb`
|
|
144
171
|
# form always. Shared here so `dispatch` and `query` (and their error
|
|
145
172
|
# messages) never drift from what a human typing `bin/run` sees.
|
|
173
|
+
#
|
|
174
|
+
# @param cli [Hash{Symbol => Object}] a `Projector.call(:cli, ...)` result
|
|
175
|
+
# @param name [String, Symbol, #to_s] the command or query name, as given by the caller
|
|
176
|
+
# @param asking [Boolean] true to resolve a query (against `cli[:questions]`),
|
|
177
|
+
# false to resolve a command (against `cli[:verbs]`)
|
|
178
|
+
# @return [Hash{Symbol => Object}] the resolved verb/question spec
|
|
179
|
+
# @raise [Runtime::NotFound] if `name` matches no known command or query
|
|
146
180
|
def resolve!(cli, name, asking:)
|
|
147
181
|
pool = asking ? cli[:questions] : cli[:verbs]
|
|
148
182
|
key = cli[:names][asking ? :question : :command][name]
|
|
@@ -153,6 +187,11 @@ module Hecks
|
|
|
153
187
|
raise Runtime::NotFound, "no such #{asking ? 'query' : 'command'}: #{name.inspect} — known: #{known}"
|
|
154
188
|
end
|
|
155
189
|
|
|
190
|
+
# Refuses a call with no summary.
|
|
191
|
+
#
|
|
192
|
+
# @param summary [String, nil] the caller-supplied one-line summary
|
|
193
|
+
# @return [void]
|
|
194
|
+
# @raise [Runtime::TypeMismatch] if `summary` is nil or blank
|
|
156
195
|
def require_summary!(summary)
|
|
157
196
|
return unless summary.nil? || summary.to_s.strip.empty?
|
|
158
197
|
|
|
@@ -160,6 +199,11 @@ module Hecks
|
|
|
160
199
|
"a one-line summary: is required on dispatch/query/state — it is what makes an audit row legible later"
|
|
161
200
|
end
|
|
162
201
|
|
|
202
|
+
# Refuses a `source:` that is not one of `SOURCE_TAGS`.
|
|
203
|
+
#
|
|
204
|
+
# @param source [String, Symbol, nil] the caller-supplied source tag
|
|
205
|
+
# @return [void]
|
|
206
|
+
# @raise [Runtime::TypeMismatch] if `source` is present and not in `SOURCE_TAGS`
|
|
163
207
|
def valid_source!(source)
|
|
164
208
|
return if source.nil? || SOURCE_TAGS.include?(source.to_s)
|
|
165
209
|
|
|
@@ -172,6 +216,11 @@ module Hecks
|
|
|
172
216
|
# `role` would silently do nothing rather than bind a real caller,
|
|
173
217
|
# which is worse than refusing: a caller who thinks they've
|
|
174
218
|
# identified themselves and haven't deserves to be told.
|
|
219
|
+
#
|
|
220
|
+
# @param role [String, Symbol, nil] the caller's self-asserted role
|
|
221
|
+
# @param actor_id [String, nil] the caller's self-asserted identity
|
|
222
|
+
# @return [void]
|
|
223
|
+
# @raise [Runtime::TypeMismatch] if `actor_id` is given without `role`
|
|
175
224
|
def valid_caller!(role, actor_id)
|
|
176
225
|
return unless actor_id && role.nil?
|
|
177
226
|
|
|
@@ -193,6 +242,12 @@ module Hecks
|
|
|
193
242
|
# is ever reached when the command declares a role and no caller is
|
|
194
243
|
# bound — so an unbound `dispatch` here means either the command
|
|
195
244
|
# declares no role at all, or a caller-side check let it through.
|
|
245
|
+
#
|
|
246
|
+
# @param role [String, Symbol, nil] the caller's self-asserted role; nil runs
|
|
247
|
+
# `block` unbound
|
|
248
|
+
# @param actor_id [String, nil] the caller's self-asserted identity
|
|
249
|
+
# @yield the dispatch/query to run, bound to the caller for its duration
|
|
250
|
+
# @return [Object] the block's own return value
|
|
196
251
|
def with_caller(role, actor_id, &block)
|
|
197
252
|
return block.call if role.nil?
|
|
198
253
|
|
|
@@ -212,6 +267,12 @@ module Hecks
|
|
|
212
267
|
# upstream of it. Refusing here, before `with_caller`/`dispatch` are
|
|
213
268
|
# ever reached, makes the bus keep the same promise: a command whose
|
|
214
269
|
# bluebook declares a role is not run through this bus without one.
|
|
270
|
+
#
|
|
271
|
+
# @param spec [Hash{Symbol => Object}] a resolved command spec (from `resolve!`),
|
|
272
|
+
# read for `:role_gated`, `:role` and `:verb`
|
|
273
|
+
# @param role [String, Symbol, nil] the caller's self-asserted role
|
|
274
|
+
# @return [void]
|
|
275
|
+
# @raise [Runtime::Unauthorized] if `spec` is role-gated and `role` is nil
|
|
215
276
|
def require_caller_for_role_gated!(spec, role)
|
|
216
277
|
return unless spec[:role_gated] && role.nil?
|
|
217
278
|
|
|
@@ -231,6 +292,15 @@ module Hecks
|
|
|
231
292
|
# kind (a bare id under one string key for :aggregate, a
|
|
232
293
|
# {aggregate:, entity:} pair of keys for :entity) — this is the one
|
|
233
294
|
# door back into it.
|
|
295
|
+
#
|
|
296
|
+
# @param envelope [Hash{Symbol => Object}] a normalized command request, with an
|
|
297
|
+
# optional `:to` route and `:with` facts
|
|
298
|
+
# @param receiver [Symbol, nil] `:aggregate`, `:entity`, or nil for anything else
|
|
299
|
+
# @param legacy_receiver [Symbol, String, Hash{Symbol => Symbol, String}, nil] where
|
|
300
|
+
# the route belongs in the flat shape: a single key for `:aggregate`, an
|
|
301
|
+
# `{aggregate:, entity:}` pair of keys for `:entity`
|
|
302
|
+
# @return [Hash{Symbol => Object}] the facts, with the route merged in under its
|
|
303
|
+
# legacy key(s) when `envelope` carries one
|
|
234
304
|
def flatten_legacy(envelope, receiver, legacy_receiver)
|
|
235
305
|
facts = envelope[:with] || {}
|
|
236
306
|
return facts unless envelope.key?(:to)
|
|
@@ -247,14 +317,34 @@ module Hecks
|
|
|
247
317
|
|
|
248
318
|
# ── the audit log `follow` reads back ───────────────────────────────
|
|
249
319
|
|
|
320
|
+
# The audit log file one domain's calls are appended to.
|
|
321
|
+
#
|
|
322
|
+
# @param domain_name [String, Symbol, #to_s] the domain name
|
|
323
|
+
# @return [String] the absolute path to that domain's JSONL log file, under
|
|
324
|
+
# `LOG_ROOT`
|
|
250
325
|
def log_path(domain_name)
|
|
251
326
|
File.join(LOG_ROOT, "#{domain_name.to_s.gsub(/[^A-Za-z0-9_-]/, '_')}.jsonl")
|
|
252
327
|
end
|
|
253
328
|
|
|
329
|
+
# Appends one call's outcome to its domain's audit log.
|
|
330
|
+
#
|
|
254
331
|
# Never fails a real call because its own audit log couldn't be
|
|
255
332
|
# written — a full disk or a permissions problem is a `follow`
|
|
256
333
|
# feature going dark, not a reason to refuse the dispatch/query/
|
|
257
334
|
# state call that was actually asked for.
|
|
335
|
+
#
|
|
336
|
+
# @param domain_name [String, Symbol, nil] the domain to log against; nil skips
|
|
337
|
+
# logging entirely (a call that never resolved a bluebook)
|
|
338
|
+
# @param tool [String] which tool logged this: `"dispatch"`, `"dry_run"`,
|
|
339
|
+
# `"query"`, or `"state"`
|
|
340
|
+
# @param summary [String, nil] the caller's one-line summary
|
|
341
|
+
# @param source [String, Symbol, nil] the caller's source tag
|
|
342
|
+
# @param outcome [Hash{Symbol => Object}] the call's own result, read for `:ok`,
|
|
343
|
+
# `:id`, `:error` and `:events`
|
|
344
|
+
# @param verb [String, nil] the resolved verb dispatched or queried, when known
|
|
345
|
+
# @param role [String, Symbol, nil] the caller's self-asserted role
|
|
346
|
+
# @param actor_id [String, nil] the caller's self-asserted identity
|
|
347
|
+
# @return [void]
|
|
258
348
|
def record!(domain_name, tool:, summary:, source:, outcome:, verb: nil, role: nil, actor_id: nil)
|
|
259
349
|
return unless domain_name
|
|
260
350
|
|
|
@@ -275,6 +365,19 @@ module Hecks
|
|
|
275
365
|
# would_succeed: false`), not a failed call. A malformed request
|
|
276
366
|
# (unknown command, a bad args shape) is still a failed call
|
|
277
367
|
# (`ok: false`) either way — it never reached the domain to be asked.
|
|
368
|
+
#
|
|
369
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
|
|
370
|
+
# @param command [String, Symbol] the command name, bare or qualified
|
|
371
|
+
# @param summary [String] a one-line human summary of the call, for the audit log
|
|
372
|
+
# @param args [Hash] the command's arguments
|
|
373
|
+
# @param source [String, Symbol, nil] a `SOURCE_TAGS` tag naming who is calling
|
|
374
|
+
# @param dry_run [Boolean] true to check whether the command would succeed,
|
|
375
|
+
# without actually dispatching it
|
|
376
|
+
# @param role [String, Symbol, nil] the caller's self-asserted role
|
|
377
|
+
# @param actor_id [String, nil] the caller's self-asserted identity
|
|
378
|
+
# @return [Hash{Symbol => Object}] `:ok` plus, on success, `:id`/`:state`/`:events`
|
|
379
|
+
# (a real dispatch) or `:would_succeed`/`:error` (a dry run); on refusal, the
|
|
380
|
+
# shape `refused` returns
|
|
278
381
|
def dispatch(runtime:, command:, summary:, args: {}, source: nil, dry_run: false, role: nil, actor_id: nil)
|
|
279
382
|
bluebook = bluebook_for(runtime)
|
|
280
383
|
tool = dry_run ? "dry_run" : "dispatch"
|
|
@@ -290,6 +393,24 @@ module Hecks
|
|
|
290
393
|
outcome
|
|
291
394
|
end
|
|
292
395
|
|
|
396
|
+
# Validates and dispatches one command, real or dry-run.
|
|
397
|
+
#
|
|
398
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
|
|
399
|
+
# @param bluebook [Bluebook::Chapter] the domain's one loaded chapter
|
|
400
|
+
# @param command [String, Symbol] the command name, bare or qualified
|
|
401
|
+
# @param summary [String, nil] a one-line human summary of the call
|
|
402
|
+
# @param args [Hash] the command's arguments, JSON-shaped
|
|
403
|
+
# @param source [String, Symbol, nil] a `SOURCE_TAGS` tag naming who is calling
|
|
404
|
+
# @param dry_run [Boolean] true to check whether the command would succeed,
|
|
405
|
+
# without actually dispatching it
|
|
406
|
+
# @param role [String, Symbol, nil] the caller's self-asserted role
|
|
407
|
+
# @param actor_id [String, nil] the caller's self-asserted identity
|
|
408
|
+
# @return [Hash{Symbol => Object}] the outcome from `real_dispatch`/`dry_run_outcome`,
|
|
409
|
+
# merged with `:verb`
|
|
410
|
+
# @raise [Runtime::TypeMismatch] if `summary`, `source`, or `actor_id` without
|
|
411
|
+
# `role` is invalid
|
|
412
|
+
# @raise [Runtime::NotFound] if `command` names no known command
|
|
413
|
+
# @raise [Runtime::Unauthorized] if `command` is role-gated and `role` is nil
|
|
293
414
|
def perform_dispatch(runtime, bluebook, command, summary, args, source, dry_run, role, actor_id)
|
|
294
415
|
require_summary!(summary)
|
|
295
416
|
valid_source!(source)
|
|
@@ -307,6 +428,14 @@ module Hecks
|
|
|
307
428
|
result.merge(verb: spec[:verb])
|
|
308
429
|
end
|
|
309
430
|
|
|
431
|
+
# Dispatches one command for real and shapes its result for the bus.
|
|
432
|
+
#
|
|
433
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
|
|
434
|
+
# @param spec [Hash{Symbol => Object}] the resolved command spec (from `resolve!`)
|
|
435
|
+
# @param envelope [Hash{Symbol => Object}] the normalized `to:`/`with:` request
|
|
436
|
+
# @param summary [String, nil] a one-line human summary of the call
|
|
437
|
+
# @return [Hash{Symbol => Object}] `:ok`, `:summary`, `:id`, `:state` (JSON-safe,
|
|
438
|
+
# nil for a port operation) and `:events` (name/payload pairs)
|
|
310
439
|
def real_dispatch(runtime, spec, envelope, summary)
|
|
311
440
|
result = runtime.dispatch_flat(spec[:verb], envelope)
|
|
312
441
|
ok(summary: summary,
|
|
@@ -315,6 +444,14 @@ module Hecks
|
|
|
315
444
|
events: result.events.map { |event| { name: event.name, payload: Facade::JsonDoor.materialize(event.payload) } })
|
|
316
445
|
end
|
|
317
446
|
|
|
447
|
+
# Checks whether one command would succeed, without dispatching it for real.
|
|
448
|
+
#
|
|
449
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
|
|
450
|
+
# @param spec [Hash{Symbol => Object}] the resolved command spec (from `resolve!`)
|
|
451
|
+
# @param envelope [Hash{Symbol => Object}] the normalized `to:`/`with:` request
|
|
452
|
+
# @param summary [String, nil] a one-line human summary of the call
|
|
453
|
+
# @return [Hash{Symbol => Object}] `:ok`, `:summary`, `:would_succeed`, and
|
|
454
|
+
# `:error` (the refusal message) when it would not succeed
|
|
318
455
|
def dry_run_outcome(runtime, spec, envelope, summary:)
|
|
319
456
|
flat = flatten_legacy(envelope, spec[:receiver], spec[:legacy_receiver])
|
|
320
457
|
runtime.dry_run?(spec[:verb], **flat)
|
|
@@ -323,6 +460,8 @@ module Hecks
|
|
|
323
460
|
ok(summary: summary, would_succeed: false, error: e.message)
|
|
324
461
|
end
|
|
325
462
|
|
|
463
|
+
# Dispatches a whole sequence of commands as one call.
|
|
464
|
+
#
|
|
326
465
|
# **One call, many steps** — the survey's own `bin/run <domain> script`
|
|
327
466
|
# shape, so an agent issuing a known sequence of commands (open an
|
|
328
467
|
# account, then fund it) pays one round trip instead of N. Every step
|
|
@@ -333,6 +472,16 @@ module Hecks
|
|
|
333
472
|
# refusing — a later step naming a record an earlier step never
|
|
334
473
|
# created will refuse honestly on its own account, which is more
|
|
335
474
|
# informative than silently dropping the rest of the batch.
|
|
475
|
+
#
|
|
476
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
|
|
477
|
+
# @param steps [Array<Hash>] each step's `command`/`args`, JSON-shaped
|
|
478
|
+
# @param summary [String] a one-line human summary of the whole batch
|
|
479
|
+
# @param source [String, Symbol, nil] a `SOURCE_TAGS` tag naming who is calling
|
|
480
|
+
# @param role [String, Symbol, nil] the caller's self-asserted role
|
|
481
|
+
# @param actor_id [String, nil] the caller's self-asserted identity
|
|
482
|
+
# @return [Hash{Symbol => Object}] `:ok` (true only if every step's own `:ok` was
|
|
483
|
+
# true), `:summary` and `:results` (each step's own `dispatch` outcome); on a
|
|
484
|
+
# refusal outside any one step, the shape `refused` returns
|
|
336
485
|
def dispatch_batch(runtime:, steps:, summary:, source: nil, role: nil, actor_id: nil)
|
|
337
486
|
require_summary!(summary)
|
|
338
487
|
results = Array(steps).map do |raw|
|
|
@@ -345,6 +494,17 @@ module Hecks
|
|
|
345
494
|
refused(e, summary: summary)
|
|
346
495
|
end
|
|
347
496
|
|
|
497
|
+
# Answers one declared query.
|
|
498
|
+
#
|
|
499
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
|
|
500
|
+
# @param question [String, Symbol] the query name, bare or qualified
|
|
501
|
+
# @param summary [String] a one-line human summary of the call, for the audit log
|
|
502
|
+
# @param args [Hash] the query's arguments
|
|
503
|
+
# @param source [String, Symbol, nil] a `SOURCE_TAGS` tag naming who is calling
|
|
504
|
+
# @param role [String, Symbol, nil] the caller's self-asserted role
|
|
505
|
+
# @param actor_id [String, nil] the caller's self-asserted identity
|
|
506
|
+
# @return [Hash{Symbol => Object}] `:ok` and `:rows` (each JSON-safe) on success,
|
|
507
|
+
# or the shape `refused` returns
|
|
348
508
|
def query(runtime:, question:, summary:, args: {}, source: nil, role: nil, actor_id: nil)
|
|
349
509
|
bluebook = bluebook_for(runtime)
|
|
350
510
|
outcome = perform_query(bluebook, runtime, question, summary, args, source, role, actor_id)
|
|
@@ -359,6 +519,20 @@ module Hecks
|
|
|
359
519
|
outcome
|
|
360
520
|
end
|
|
361
521
|
|
|
522
|
+
# Validates and answers one query.
|
|
523
|
+
#
|
|
524
|
+
# @param bluebook [Bluebook::Chapter] the domain's one loaded chapter
|
|
525
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
|
|
526
|
+
# @param question [String, Symbol] the query name, bare or qualified
|
|
527
|
+
# @param summary [String, nil] a one-line human summary of the call
|
|
528
|
+
# @param args [Hash] the query's arguments, JSON-shaped
|
|
529
|
+
# @param source [String, Symbol, nil] a `SOURCE_TAGS` tag naming who is calling
|
|
530
|
+
# @param role [String, Symbol, nil] the caller's self-asserted role
|
|
531
|
+
# @param actor_id [String, nil] the caller's self-asserted identity
|
|
532
|
+
# @return [Hash{Symbol => Object}] `:ok`, `:rows` (each JSON-safe) and `:verb`
|
|
533
|
+
# @raise [Runtime::TypeMismatch] if `summary`, `source`, or `actor_id` without
|
|
534
|
+
# `role` is invalid
|
|
535
|
+
# @raise [Runtime::NotFound] if `question` names no known query
|
|
362
536
|
def perform_query(bluebook, runtime, question, summary, args, source, role, actor_id)
|
|
363
537
|
require_summary!(summary)
|
|
364
538
|
valid_source!(source)
|
|
@@ -370,12 +544,22 @@ module Hecks
|
|
|
370
544
|
ok(summary: summary, rows: rows.map { |row| Facade::JsonDoor.materialize(row) }).merge(verb: spec[:verb])
|
|
371
545
|
end
|
|
372
546
|
|
|
547
|
+
# Reads one aggregate's stored records directly, bypassing any declared query.
|
|
548
|
+
#
|
|
373
549
|
# **What is actually stored** — no verb, no interpretation, the repository
|
|
374
550
|
# itself. `id:` given answers one record (`NotFound` when it names
|
|
375
551
|
# nothing); omitted answers every record the aggregate currently
|
|
376
552
|
# holds. This is the difference `query` can't cover: a query answers a
|
|
377
553
|
# declared question, and an aggregate that never declared "list
|
|
378
554
|
# everything" has no query this could reuse.
|
|
555
|
+
#
|
|
556
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
|
|
557
|
+
# @param aggregate [String, Symbol] the aggregate's declared name
|
|
558
|
+
# @param summary [String] a one-line human summary of the call, for the audit log
|
|
559
|
+
# @param id [String, Object, nil] one record's identity, or nil for every record
|
|
560
|
+
# @param source [String, Symbol, nil] a `SOURCE_TAGS` tag naming who is calling
|
|
561
|
+
# @return [Hash{Symbol => Object}] `:ok` and, with `id:`, `:record`; without it,
|
|
562
|
+
# `:count` and `:records` (each JSON-safe); or the shape `refused` returns
|
|
379
563
|
def state(runtime:, aggregate:, summary:, id: nil, source: nil)
|
|
380
564
|
bluebook = bluebook_for(runtime)
|
|
381
565
|
outcome = perform_state(runtime, bluebook, aggregate, summary, id)
|
|
@@ -388,6 +572,18 @@ module Hecks
|
|
|
388
572
|
outcome
|
|
389
573
|
end
|
|
390
574
|
|
|
575
|
+
# Validates and reads one aggregate's stored record(s).
|
|
576
|
+
#
|
|
577
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
|
|
578
|
+
# @param bluebook [Bluebook::Chapter] the domain's one loaded chapter
|
|
579
|
+
# @param aggregate [String, Symbol] the aggregate's declared name
|
|
580
|
+
# @param summary [String, nil] a one-line human summary of the call
|
|
581
|
+
# @param id [String, Object, nil] one record's identity, or nil for every record
|
|
582
|
+
# @return [Hash{Symbol => Object}] `:ok` and, with `id`, `:record`; without it,
|
|
583
|
+
# `:count` and `:records` (each JSON-safe)
|
|
584
|
+
# @raise [Runtime::TypeMismatch] if `summary` is invalid
|
|
585
|
+
# @raise [Runtime::NotFound] if `aggregate` names no known aggregate, or `id`
|
|
586
|
+
# names no record of it
|
|
391
587
|
def perform_state(runtime, bluebook, aggregate, summary, id)
|
|
392
588
|
require_summary!(summary)
|
|
393
589
|
ir = aggregate_ir!(bluebook, aggregate)
|
|
@@ -405,6 +601,8 @@ module Hecks
|
|
|
405
601
|
|
|
406
602
|
# ── the four zoom levels ─────────────────────────────────────────
|
|
407
603
|
|
|
604
|
+
# Lists every domain directory under a root.
|
|
605
|
+
#
|
|
408
606
|
# **Zoom level zero** — every domain directory a root actually holds,
|
|
409
607
|
# discovered rather than typed from memory. Every other tool takes
|
|
410
608
|
# `domain:` as a directory it assumes the caller already knows; this
|
|
@@ -412,6 +610,12 @@ module Hecks
|
|
|
412
610
|
# is the same predicate `domain_root`/`nearest_domain` already walk
|
|
413
611
|
# up directories checking — a bare `.hecksagon` or one under
|
|
414
612
|
# `bluebook/`, the two real shapes this corpus uses.
|
|
613
|
+
#
|
|
614
|
+
# @param under [String] the directory to search, relative to `BOOT_ROOT`
|
|
615
|
+
# @return [Hash{Symbol => Object}] `:ok`, `:under`, and `:domains` (each found
|
|
616
|
+
# domain's path, relative to `BOOT_ROOT`, sorted); `:domains` is `[]` when
|
|
617
|
+
# `under` does not exist
|
|
618
|
+
# @raise [Runtime::TypeMismatch] if `under` resolves outside `BOOT_ROOT`
|
|
415
619
|
def domains(under: "examples")
|
|
416
620
|
root = confine!(under, "under")
|
|
417
621
|
return ok(under: under, domains: []) unless Dir.exist?(root)
|
|
@@ -422,10 +626,17 @@ module Hecks
|
|
|
422
626
|
ok(under: under, domains: found.map { |name| File.join(under, name) })
|
|
423
627
|
end
|
|
424
628
|
|
|
629
|
+
# Lists a domain's aggregates and their command/query names.
|
|
630
|
+
#
|
|
425
631
|
# **Zoom level one** — every aggregate this domain declares, and every
|
|
426
632
|
# command/query name each answers to, snake_cased exactly as
|
|
427
633
|
# `dispatch`/`query` want it. Enough to pick a target; `describe` is
|
|
428
634
|
# the next level down for what one of them actually takes.
|
|
635
|
+
#
|
|
636
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
|
|
637
|
+
# @return [Hash{Symbol => Object}] `:ok`, `:domain`, and `:aggregates` — an Array
|
|
638
|
+
# of `{name:, commands:, queries:}` Hashes, commands and queries sorted,
|
|
639
|
+
# command names suffixed `!`; or the shape `refused` returns
|
|
429
640
|
def catalog(runtime:)
|
|
430
641
|
bluebook = bluebook_for(runtime)
|
|
431
642
|
|
|
@@ -439,12 +650,21 @@ module Hecks
|
|
|
439
650
|
refused(e)
|
|
440
651
|
end
|
|
441
652
|
|
|
653
|
+
# Answers one aggregate's (or the whole chapter's) full usage documentation.
|
|
654
|
+
#
|
|
442
655
|
# **Zoom level two** — the exact same usage document a human gets from
|
|
443
656
|
# `bin/docs <domain> [aggregate]` (`Projector::DocsProjector`, the
|
|
444
657
|
# identical projection `Surface::AggregateDoor#docs` calls one door
|
|
445
658
|
# over): every command's arguments, the states it may be issued
|
|
446
659
|
# from, and every way it can refuse. `aggregate:` omitted answers the
|
|
447
660
|
# whole chapter.
|
|
661
|
+
#
|
|
662
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
|
|
663
|
+
# @param aggregate [String, Symbol, nil] one aggregate's name, or nil for the
|
|
664
|
+
# whole chapter
|
|
665
|
+
# @return [Hash{Symbol => Object}] `:ok`, `:domain` and `:docs` (the rendered
|
|
666
|
+
# documentation); or the shape `refused` returns
|
|
667
|
+
# @raise [Runtime::NotFound] if `aggregate` names no known aggregate
|
|
448
668
|
def describe(runtime:, aggregate: nil)
|
|
449
669
|
bluebook = bluebook_for(runtime)
|
|
450
670
|
options = aggregate ? { aggregate: aggregate_ir!(bluebook, aggregate).hecks_name } : {}
|
|
@@ -454,6 +674,8 @@ module Hecks
|
|
|
454
674
|
refused(e)
|
|
455
675
|
end
|
|
456
676
|
|
|
677
|
+
# Boots a domain and reports whether its wiring (and, optionally, its logic) is sound.
|
|
678
|
+
#
|
|
457
679
|
# **Zoom level three** — is the wiring sound at all: every bind names a
|
|
458
680
|
# declared aggregate, every adapter satisfies the port it claims, the
|
|
459
681
|
# default adapter is usable. `Registry#verify!` (`runtime/registry/
|
|
@@ -479,6 +701,12 @@ module Hecks
|
|
|
479
701
|
# much "not valid" as a real wiring mismatch, and this tool exists
|
|
480
702
|
# precisely so none of those ever cross a projection of this bus as
|
|
481
703
|
# a crash.
|
|
704
|
+
#
|
|
705
|
+
# @param domain [String] the domain's directory path, relative to `BOOT_ROOT`
|
|
706
|
+
# @param deep [Boolean] also run `Bluebook::ModelCheck` past wiring, into logic
|
|
707
|
+
# @return [Hash{Symbol => Object}] `{ok: true, domain:, valid: true}`, plus
|
|
708
|
+
# `:findings` (each `{kind:, severity:, subject:, message:}`) when `deep`; or
|
|
709
|
+
# `{ok: false, domain:, valid: false, error:}` if the boot itself failed
|
|
482
710
|
def validate(domain:, deep: false)
|
|
483
711
|
runtime = Hecks.boot(confine!(domain, "domain"), install_facade: false)
|
|
484
712
|
result = { ok: true, domain: domain, valid: true }
|
|
@@ -497,10 +725,19 @@ module Hecks
|
|
|
497
725
|
# ── beyond the zoom levels ──────────────────────────────────────────
|
|
498
726
|
|
|
499
727
|
# The full write history, not just the current head — `bin/history`'s
|
|
500
|
-
# own logic, unchanged:
|
|
501
|
-
#
|
|
502
|
-
#
|
|
503
|
-
#
|
|
728
|
+
# own logic, unchanged: every operation that ever touched the
|
|
729
|
+
# aggregate, read off its repository's own `entries`. `Registry#repository`
|
|
730
|
+
# always hands back a `Ports::Persistence::AppendOnly`-wrapped adapter,
|
|
731
|
+
# never the bare adapter, so this never needs to guard against one —
|
|
732
|
+
# what varies by adapter is only how much the wrapped `entries` itself
|
|
733
|
+
# actually holds: a fresh Memory boot honestly has none yet, and a
|
|
734
|
+
# durable adapter (Heki, Postgres, SQLite, D1) has whatever it
|
|
735
|
+
# journaled.
|
|
736
|
+
#
|
|
737
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
|
|
738
|
+
# @return [Hash{Symbol => Object}] `:ok`, `:domain` and `:history` (each
|
|
739
|
+
# aggregate's storage name mapped to its `journal_entries`); or the shape
|
|
740
|
+
# `refused` returns
|
|
504
741
|
def history(runtime:)
|
|
505
742
|
bluebook = bluebook_for(runtime)
|
|
506
743
|
entries = bluebook.aggregates.each_with_object({}) do |aggregate, all|
|
|
@@ -513,9 +750,13 @@ module Hecks
|
|
|
513
750
|
refused(e)
|
|
514
751
|
end
|
|
515
752
|
|
|
753
|
+
# One aggregate's full append-only write history, JSON-safe.
|
|
754
|
+
#
|
|
755
|
+
# @param repository [Ports::Persistence::AppendOnly] the aggregate's repository, as
|
|
756
|
+
# `Registry#repository` always returns it
|
|
757
|
+
# @return [Array<Hash>] each journal entry's `:operation`, `:id` and `:state`
|
|
758
|
+
# (materialized); `[]` when the adapter has journaled nothing yet
|
|
516
759
|
def journal_entries(repository)
|
|
517
|
-
return [] unless repository.is_a?(Ports::Persistence::AppendOnly)
|
|
518
|
-
|
|
519
760
|
repository.entries.map { |entry| { operation: entry.operation, id: entry.id, state: Facade::JsonDoor.materialize(entry.state) } }
|
|
520
761
|
end
|
|
521
762
|
|
|
@@ -526,6 +767,13 @@ module Hecks
|
|
|
526
767
|
# itself — `target:` names a `.behaviors` file or a directory to
|
|
527
768
|
# sweep, never a `runtime:`, the one other method here besides
|
|
528
769
|
# `validate` that takes a path instead.
|
|
770
|
+
#
|
|
771
|
+
# @param target [String] a `.behaviors` file's path, or a directory to sweep
|
|
772
|
+
# for every `.behaviors` file under it
|
|
773
|
+
# @return [Hash{Symbol => Object}] `:ok`, `:target`, `:files` (each file's own
|
|
774
|
+
# `behaviors_file` shape) and `:counts` (`Behaviors.summarize`'s tally); or
|
|
775
|
+
# the shape `refused` returns
|
|
776
|
+
# @raise [Runtime::NotFound] if `target` is nil or names no real file or directory
|
|
529
777
|
def behaviors(target:)
|
|
530
778
|
require_relative "behaviors"
|
|
531
779
|
raise Runtime::NotFound, "no such file or directory: #{target.inspect}" unless target && File.exist?(target)
|
|
@@ -541,6 +789,11 @@ module Hecks
|
|
|
541
789
|
refused(e)
|
|
542
790
|
end
|
|
543
791
|
|
|
792
|
+
# Shapes one `.behaviors` file's own result for the bus.
|
|
793
|
+
#
|
|
794
|
+
# @param result [Behaviors::FileResult] one file's run result
|
|
795
|
+
# @return [Hash{Symbol => Object}] `:path`, `:parse_error`, and `:runs` (each
|
|
796
|
+
# test's `description`/`status`/`message`)
|
|
544
797
|
def behaviors_file(result)
|
|
545
798
|
{ path: result.path,
|
|
546
799
|
parse_error: result.parse_error,
|
|
@@ -555,6 +808,12 @@ module Hecks
|
|
|
555
808
|
# dry-run call appends to (`record!`), tailed back here. Still real,
|
|
556
809
|
# still cross-process — the log outlives any one door's own process —
|
|
557
810
|
# just pull instead of push.
|
|
811
|
+
#
|
|
812
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
|
|
813
|
+
# @param limit [Integer, #to_i] how many of the most recent log entries to return;
|
|
814
|
+
# clamped to at least 1
|
|
815
|
+
# @return [Hash{Symbol => Object}] `:ok`, `:domain` and `:entries` (each logged
|
|
816
|
+
# call's own JSON entry); or the shape `refused` returns
|
|
558
817
|
def follow(runtime:, limit: 20)
|
|
559
818
|
bluebook = bluebook_for(runtime)
|
|
560
819
|
entries = log_lines(bluebook.name).last([limit.to_i, 1].max)
|
|
@@ -564,6 +823,11 @@ module Hecks
|
|
|
564
823
|
refused(e)
|
|
565
824
|
end
|
|
566
825
|
|
|
826
|
+
# Reads back one domain's whole audit log.
|
|
827
|
+
#
|
|
828
|
+
# @param domain_name [String, Symbol, #to_s] the domain name
|
|
829
|
+
# @return [Array<Hash>] every logged entry, parsed with Symbol keys, in log
|
|
830
|
+
# order; `[]` if the domain has no log file yet
|
|
567
831
|
def log_lines(domain_name)
|
|
568
832
|
path = log_path(domain_name)
|
|
569
833
|
return [] unless File.exist?(path)
|
|
@@ -586,6 +850,17 @@ module Hecks
|
|
|
586
850
|
# `aggregate:` narrows to one aggregate; `id:` (requires
|
|
587
851
|
# `aggregate:` — an id alone is not unique across aggregates)
|
|
588
852
|
# narrows to one record's own events.
|
|
853
|
+
#
|
|
854
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
|
|
855
|
+
# @param aggregate [String, Symbol, nil] narrows to one aggregate's own events
|
|
856
|
+
# @param id [String, Object, nil] narrows to one record's own events; requires
|
|
857
|
+
# `aggregate`
|
|
858
|
+
# @param limit [Integer, #to_i, nil] how many of the most recent matching events
|
|
859
|
+
# to return; nil returns every one found
|
|
860
|
+
# @return [Hash{Symbol => Object}] `:ok`, `:domain` and `:events` (each with
|
|
861
|
+
# `:name`, `:payload`, `:time`, `:verb` and `:id`); or the shape `refused` returns
|
|
862
|
+
# @raise [Runtime::TypeMismatch] if `id` is given without `aggregate`
|
|
863
|
+
# @raise [Runtime::NotFound] if `aggregate` names no known aggregate
|
|
589
864
|
def events(runtime:, aggregate: nil, id: nil, limit: nil)
|
|
590
865
|
raise Runtime::TypeMismatch, "id: requires aggregate: too — an id alone is not unique across aggregates" if id && !aggregate
|
|
591
866
|
|
|
@@ -606,6 +881,14 @@ module Hecks
|
|
|
606
881
|
# self-contained per-entry check with nothing to share with its
|
|
607
882
|
# neighbors, extracted only to keep `events` itself to the query's
|
|
608
883
|
# own shape: build the filter, apply the limit, wrap the result.
|
|
884
|
+
#
|
|
885
|
+
# @param entry [Hash{Symbol => Object}] one logged audit entry
|
|
886
|
+
# @param fqn [String, nil] narrows to a dispatch whose verb starts with this
|
|
887
|
+
# aggregate FQN; nil matches any aggregate
|
|
888
|
+
# @param id [String, Object, nil] narrows to a dispatch whose settled record has
|
|
889
|
+
# this id; nil matches any record
|
|
890
|
+
# @return [Array<Hash>, nil] `entry`'s own events (each with `:time`, `:verb` and
|
|
891
|
+
# `:id` merged in), or nil if `entry` isn't a matching successful dispatch
|
|
609
892
|
def entry_events(entry, fqn, id)
|
|
610
893
|
return unless entry[:tool] == "dispatch" && entry[:ok] && entry[:events]
|
|
611
894
|
return if fqn && !entry[:verb].to_s.start_with?("#{fqn}.")
|
|
@@ -624,16 +907,29 @@ module Hecks
|
|
|
624
907
|
# port verb) is exactly the shape this bus promises never crashes
|
|
625
908
|
# through it. `dry_run_outcome` already treats it this way locally;
|
|
626
909
|
# this makes every other caller of `refusal_classes` do the same.
|
|
910
|
+
#
|
|
911
|
+
# @return [Array<Class>] every exception class this bus catches and reshapes
|
|
912
|
+
# into an honest refusal rather than letting crash through
|
|
627
913
|
def refusal_classes = [Runtime::NotFound, Runtime::TypeMismatch, Runtime::WiringError, *Runtime::DOMAIN_REFUSALS]
|
|
628
914
|
|
|
915
|
+
# Builds a successful outcome Hash.
|
|
916
|
+
#
|
|
917
|
+
# @param fields [Hash{Symbol => Object}] fields to merge in alongside `ok: true`
|
|
918
|
+
# @return [Hash{Symbol => Object}] `{ok: true}` merged with `fields`
|
|
629
919
|
def ok(**fields) = { ok: true }.merge(fields)
|
|
630
920
|
|
|
921
|
+
# Builds a refused outcome Hash from a caught exception.
|
|
922
|
+
#
|
|
631
923
|
# **An honest refusal, not a crash** — the survey's own item #9: "an
|
|
632
924
|
# explicit, structured refusal a caller can act on" rather than a
|
|
633
925
|
# stack trace an agent has to parse to find the one line that
|
|
634
926
|
# mattered. The domain's own refusal text travels verbatim
|
|
635
927
|
# (`RefusalWording` already renders every one of these to be read),
|
|
636
928
|
# this only wraps it consistently.
|
|
929
|
+
#
|
|
930
|
+
# @param error [StandardError] the caught exception, a member of `refusal_classes`
|
|
931
|
+
# @param summary [String, nil] the caller's one-line summary
|
|
932
|
+
# @return [Hash{Symbol => Object}] `{ok: false, summary:, error:}`
|
|
637
933
|
def refused(error, summary: nil) = { ok: false, summary: summary, error: error.message }
|
|
638
934
|
end
|
|
639
935
|
end
|