hecks 2.7.0 → 2.9.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/exe/hecks +11 -0
- data/lib/hecks/adapters/driven/claude_code.rb +4 -104
- data/lib/hecks/adapters/driven/d1.rb +44 -237
- data/lib/hecks/adapters/driven/folder.rb +17 -76
- data/lib/hecks/adapters/driven/google_authentication.rb +15 -49
- data/lib/hecks/adapters/driven/governance_authorization.rb +23 -70
- data/lib/hecks/adapters/driven/heki/journal.rb +8 -41
- data/lib/hecks/adapters/driven/heki/saga_store.rb +12 -54
- data/lib/hecks/adapters/driven/heki/snapshot.rb +4 -12
- data/lib/hecks/adapters/driven/heki.rb +13 -42
- data/lib/hecks/adapters/driven/identity_registry.rb +3 -14
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +9 -26
- data/lib/hecks/adapters/driven/in_process_key_vault.rb +2 -14
- data/lib/hecks/adapters/driven/lambda/client.rb +10 -51
- data/lib/hecks/adapters/driven/lambda.rb +10 -84
- data/lib/hecks/adapters/driven/local_storage.rb +5 -40
- data/lib/hecks/adapters/driven/memory.rb +18 -99
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +5 -21
- data/lib/hecks/adapters/driven/postgres/codec.rb +6 -28
- data/lib/hecks/adapters/driven/postgres/outbox.rb +3 -12
- data/lib/hecks/adapters/driven/postgres/reconnect.rb +4 -30
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +34 -108
- data/lib/hecks/adapters/driven/postgres.rb +145 -134
- data/lib/hecks/adapters/driven/postgres_era.adapter +4 -15
- data/lib/hecks/adapters/driven/prism.rb +7 -30
- data/lib/hecks/adapters/driven/sql_query_builder.rb +21 -121
- data/lib/hecks/adapters/driven/sqlite/codec.rb +8 -40
- data/lib/hecks/adapters/driven/sqlite/projection.rb +26 -82
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +40 -84
- data/lib/hecks/adapters/driven/sqlite.rb +128 -65
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +15 -37
- data/lib/hecks/adapters/driven.rb +5 -36
- data/lib/hecks/adapters/driving/github_webhook.rb +8 -76
- data/lib/hecks/behaviors/dsl.rb +7 -25
- data/lib/hecks/behaviors/expectations.rb +32 -142
- data/lib/hecks/behaviors/ir.rb +7 -20
- data/lib/hecks/behaviors/rspec.rb +6 -17
- data/lib/hecks/behaviors/runner.rb +10 -34
- data/lib/hecks/behaviors.rb +4 -8
- data/lib/hecks/bluebook/aggregate.rb +9 -75
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +9 -38
- data/lib/hecks/bluebook/assembly/build.rb +4 -15
- data/lib/hecks/bluebook/assembly/contract.rb +15 -136
- data/lib/hecks/bluebook/assembly/contracts.rb +54 -174
- data/lib/hecks/bluebook/assembly/marks.rb +31 -223
- data/lib/hecks/bluebook/assembly/specializer.rb +6 -52
- data/lib/hecks/bluebook/assembly.rb +6 -45
- data/lib/hecks/bluebook/attribute.rb +12 -61
- data/lib/hecks/bluebook/behaviour/aggregate.rb +10 -39
- data/lib/hecks/bluebook/behaviour/attribute.rb +3 -14
- data/lib/hecks/bluebook/behaviour/chapter.rb +17 -42
- data/lib/hecks/bluebook/behaviour/command.rb +14 -66
- data/lib/hecks/bluebook/behaviour/domain_port.rb +4 -20
- data/lib/hecks/bluebook/behaviour/entity.rb +7 -27
- data/lib/hecks/bluebook/behaviour/hexagon.rb +23 -41
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +15 -40
- data/lib/hecks/bluebook/behaviour/policy.rb +23 -52
- data/lib/hecks/bluebook/behaviour/process_manager.rb +30 -57
- data/lib/hecks/bluebook/behaviour/read_model.rb +38 -26
- data/lib/hecks/bluebook/behaviour/traits.rb +27 -64
- data/lib/hecks/bluebook/behaviour/value_object.rb +5 -24
- data/lib/hecks/bluebook/capabilities.rb +17 -49
- data/lib/hecks/bluebook/chapter.rb +10 -48
- data/lib/hecks/bluebook/command.rb +23 -81
- data/lib/hecks/bluebook/domain_port.rb +14 -63
- data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +50 -229
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +50 -377
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +33 -204
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +17 -65
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +86 -708
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +56 -170
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +1 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +74 -546
- data/lib/hecks/bluebook/dsl/const_shim.rb +8 -41
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +12 -87
- data/lib/hecks/bluebook/dsl/entity_builder.rb +44 -330
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +20 -269
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +26 -189
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +13 -88
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +6 -20
- data/lib/hecks/bluebook/dsl/policy_builder.rb +16 -88
- data/lib/hecks/bluebook/dsl/port_builder.rb +2 -6
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +22 -96
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +31 -184
- data/lib/hecks/bluebook/dsl/query_builder.rb +11 -42
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +75 -201
- data/lib/hecks/bluebook/dsl/rule_reference.rb +22 -163
- data/lib/hecks/bluebook/dsl/translation_builder.rb +28 -114
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +10 -124
- data/lib/hecks/bluebook/dsl/word_gate.rb +16 -162
- data/lib/hecks/bluebook/dsl/world_builder.rb +19 -106
- data/lib/hecks/bluebook/entity.rb +13 -71
- data/lib/hecks/bluebook/expression/ast_json.rb +24 -190
- data/lib/hecks/bluebook/expression/ast_reader.rb +12 -54
- data/lib/hecks/bluebook/expression/canonical_form.rb +11 -88
- data/lib/hecks/bluebook/expression/evaluator.rb +47 -295
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +30 -147
- data/lib/hecks/bluebook/expression/resolver.rb +37 -660
- data/lib/hecks/bluebook/hexagon.rb +5 -28
- data/lib/hecks/bluebook/lifecycle.rb +5 -16
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -7
- data/lib/hecks/bluebook/meta_validator/judge.rb +111 -361
- data/lib/hecks/bluebook/meta_validator/plan.rb +33 -162
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -7
- data/lib/hecks/bluebook/meta_validator/readings.rb +56 -342
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +24 -176
- data/lib/hecks/bluebook/meta_validator/shapes.rb +54 -268
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +47 -308
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +7 -43
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -18
- data/lib/hecks/bluebook/meta_validator.rb +79 -349
- data/lib/hecks/bluebook/model_check/client_profile.rb +10 -91
- data/lib/hecks/bluebook/model_check.rb +102 -600
- data/lib/hecks/bluebook/pattern_subset.rb +11 -69
- data/lib/hecks/bluebook/process_manager.rb +13 -83
- data/lib/hecks/bluebook/project_loader.rb +3 -39
- data/lib/hecks/bluebook/project_register.rb +12 -60
- data/lib/hecks/bluebook/query.rb +9 -48
- data/lib/hecks/bluebook/read_model.rb +57 -81
- data/lib/hecks/bluebook/reference.rb +7 -41
- data/lib/hecks/bluebook/smoke_test.rb +8 -98
- data/lib/hecks/bluebook/synthesizer.rb +13 -62
- data/lib/hecks/bluebook/translation.rb +26 -62
- data/lib/hecks/bluebook/value_object.rb +11 -36
- data/lib/hecks/bluebook.rb +5 -22
- data/lib/hecks/cache_dir.rb +84 -0
- data/lib/hecks/cli/document.rb +86 -0
- data/lib/hecks/cli/ir.rb +45 -0
- data/lib/hecks/cli/mcp.rb +31 -0
- data/lib/hecks/cli/mcp_door.rb +366 -0
- data/lib/hecks/cli/model_check.rb +203 -0
- data/lib/hecks/cli/project_cli.rb +90 -0
- data/lib/hecks/cli/project_diagrams.rb +36 -0
- data/lib/hecks/cli/run.rb +173 -0
- data/lib/hecks/cli/smoke_test.rb +45 -0
- data/lib/hecks/cli/stores.rb +53 -0
- data/lib/hecks/cli.rb +160 -0
- data/lib/hecks/construct.rb +8 -34
- data/lib/hecks/deploy/bluebook/deploy.bluebook +33 -142
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +2 -9
- data/lib/hecks/embryonaut_bluebook/lock.rb +3 -35
- data/lib/hecks/embryonaut_bluebook/shape.rb +1 -17
- data/lib/hecks/embryonaut_bluebook/vendor.rb +8 -53
- data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +4 -19
- data/lib/hecks/embryonaut_bluebook.rb +4 -71
- data/lib/hecks/facade/cli_door.rb +24 -106
- data/lib/hecks/facade/cli_runner.rb +28 -172
- data/lib/hecks/facade/command_request.rb +13 -33
- data/lib/hecks/facade/handle.rb +34 -121
- data/lib/hecks/facade/json_door.rb +31 -146
- data/lib/hecks/facade/surface/aggregate_door.rb +39 -104
- data/lib/hecks/facade/surface/chapter.rb +10 -54
- data/lib/hecks/facade/surface.rb +8 -23
- data/lib/hecks/facade.rb +10 -25
- data/lib/hecks/forms/app.rb +15 -70
- data/lib/hecks/forms/command_form_renderer.rb +10 -29
- data/lib/hecks/forms/field_renderer.rb +17 -156
- data/lib/hecks/forms/field_shape.rb +30 -222
- data/lib/hecks/forms/html.rb +12 -47
- data/lib/hecks/forms/index_renderer.rb +1 -4
- data/lib/hecks/forms/page.rb +3 -9
- data/lib/hecks/forms/params.rb +14 -60
- data/lib/hecks/forms/port_argument.rb +2 -19
- data/lib/hecks/forms/query_form_renderer.rb +4 -79
- data/lib/hecks/forms/record_renderer.rb +8 -75
- data/lib/hecks/forms/record_table.rb +7 -50
- data/lib/hecks/forms/reference_options.rb +3 -30
- data/lib/hecks/forms/value_object_shape.rb +8 -35
- data/lib/hecks/forms.rb +5 -33
- data/lib/hecks/fqn.rb +5 -12
- data/lib/hecks/framework/bluebook/compliance.bluebook +16 -65
- data/lib/hecks/framework/bluebook/console_settings.bluebook +47 -134
- data/lib/hecks/framework/bluebook/framework.hecksagon +5 -14
- data/lib/hecks/framework/bluebook/governance.bluebook +13 -34
- data/lib/hecks/framework/bluebook/identity.bluebook +11 -26
- data/lib/hecks/framework/bluebook/privacy.bluebook +8 -33
- data/lib/hecks/framework.rb +11 -80
- data/lib/hecks/freezer.rb +5 -39
- data/lib/hecks/grammar/expression.bluebook +15 -40
- data/lib/hecks/grammar/translation.bluebook +3 -6
- data/lib/hecks/grammar.rb +16 -41
- data/lib/hecks/ir.rb +14 -83
- data/lib/hecks/language/adapter.bluebook +2 -7
- data/lib/hecks/language/bluebook/aggregate.bluebook +56 -221
- data/lib/hecks/language/bluebook/attaches/paging.bluebook +6 -15
- data/lib/hecks/language/bluebook/bluebook.bluebook +15 -83
- data/lib/hecks/language/bluebook/bluebook.hecksagon +3 -15
- data/lib/hecks/language/bluebook/command.bluebook +27 -144
- data/lib/hecks/language/bluebook/entity.bluebook +24 -97
- data/lib/hecks/language/bluebook/policy.bluebook +16 -67
- data/lib/hecks/language/bluebook/process_manager.bluebook +28 -151
- data/lib/hecks/language/bluebook/projection.bluebook +126 -60
- data/lib/hecks/language/bluebook/query.bluebook +12 -43
- data/lib/hecks/language/bluebook/shape.bluebook +23 -73
- data/lib/hecks/language/bluebook/syntax.bluebook +51 -262
- data/lib/hecks/language/bluebook/vocabulary.bluebook +98 -197
- data/lib/hecks/language/hecksagon/adapter_binding.bluebook +3 -10
- data/lib/hecks/language/hecksagon/domain_port.bluebook +4 -19
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +6 -25
- data/lib/hecks/language/hecksagon/port_operation.bluebook +3 -14
- data/lib/hecks/language/oidc.json +35 -0
- data/lib/hecks/language/port.bluebook +4 -17
- data/lib/hecks/language/translation/translation.bluebook +3 -9
- data/lib/hecks/language/translation/translation_aggregate.bluebook +5 -24
- data/lib/hecks/language/world/wiring.bluebook +1 -3
- data/lib/hecks/language/world/world.bluebook +5 -7
- data/lib/hecks/literal.rb +8 -50
- data/lib/hecks/mcp_door_scope.rb +95 -0
- data/lib/hecks/mcp_stdio_guard.rb +4 -41
- data/lib/hecks/naming.rb +29 -137
- data/lib/hecks/ports/access_control.rb +7 -23
- data/lib/hecks/ports/agent/answers.rb +5 -23
- data/lib/hecks/ports/agent.rb +47 -172
- data/lib/hecks/ports/authentication.rb +12 -39
- data/lib/hecks/ports/authorization.rb +16 -43
- data/lib/hecks/ports/clock.rb +5 -45
- data/lib/hecks/ports/extraction.rb +7 -12
- data/lib/hecks/ports/identity_assignment.rb +8 -29
- data/lib/hecks/ports/identity_generation.rb +6 -29
- data/lib/hecks/ports/identity_resolution.rb +2 -10
- data/lib/hecks/ports/key_vault.rb +9 -45
- data/lib/hecks/ports/persistence/append_only.rb +62 -65
- data/lib/hecks/ports/persistence/binding_policy.rb +4 -14
- data/lib/hecks/ports/persistence/codec_boundary.rb +20 -80
- data/lib/hecks/ports/persistence/null_saga_store.rb +4 -20
- data/lib/hecks/ports/persistence/plugin.rb +13 -42
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +38 -141
- data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +15 -115
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +16 -84
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +7 -26
- data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +3 -22
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +55 -181
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/events.rb +77 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +12 -122
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +28 -128
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +73 -474
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +31 -124
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +45 -219
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +13 -108
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +18 -75
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +9 -49
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +6 -123
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +23 -55
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +14 -72
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +7 -16
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +14 -115
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +13 -45
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +127 -554
- data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +13 -63
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +2 -17
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +3 -14
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +13 -58
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -17
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +9 -26
- data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +9 -25
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +22 -122
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +34 -123
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +5 -12
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +3 -9
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -14
- data/lib/hecks/ports/persistence/plugins/era.rb +4 -23
- data/lib/hecks/ports/persistence/postgres_dump/connection.rb +7 -23
- data/lib/hecks/ports/persistence/postgres_dump.rb +4 -35
- data/lib/hecks/ports/persistence/remote_runtime.rb +2 -24
- data/lib/hecks/ports/persistence/repository_factory.rb +8 -27
- data/lib/hecks/ports/persistence/state_codec.rb +23 -103
- data/lib/hecks/ports/persistence.rb +7 -14
- data/lib/hecks/ports/projection.rb +42 -84
- data/lib/hecks/ports/query/in_memory.rb +10 -58
- data/lib/hecks/ports/query/ordering.rb +11 -32
- data/lib/hecks/ports/query.rb +14 -37
- data/lib/hecks/ports.rb +2 -8
- data/lib/hecks/projections/bootstrap_table.rb +11 -43
- data/lib/hecks/projections/deploy/fargate/assembly.rb +2 -15
- data/lib/hecks/projections/deploy/fargate/cdn.rb +1 -48
- data/lib/hecks/projections/deploy/fargate/cdn_default.rb +2 -11
- data/lib/hecks/projections/deploy/fargate/check.rb +5 -14
- data/lib/hecks/projections/deploy/fargate/containers.rb +3 -78
- data/lib/hecks/projections/deploy/fargate/extras.rb +3 -44
- data/lib/hecks/projections/deploy/fargate/monitoring.rb +0 -44
- data/lib/hecks/projections/deploy/fargate/settings.rb +2 -54
- data/lib/hecks/projections/deploy/fargate/yaml.rb +13 -58
- data/lib/hecks/projections/deploy/fargate.rb +35 -202
- data/lib/hecks/projections/deploy/lambda.rb +151 -679
- data/lib/hecks/projections/deploy/preview/access_yaml.rb +1 -33
- data/lib/hecks/projections/deploy/preview/containers.rb +6 -111
- data/lib/hecks/projections/deploy/preview/routing_yaml.rb +4 -56
- data/lib/hecks/projections/deploy/preview/script.rb +2 -14
- data/lib/hecks/projections/deploy/preview/settings.rb +3 -33
- data/lib/hecks/projections/deploy/preview/task_yaml.rb +2 -38
- data/lib/hecks/projections/deploy/preview/template.rb +2 -58
- data/lib/hecks/projections/deploy/preview.rb +2 -67
- data/lib/hecks/projections/deploy/scripts/settings.rb +3 -25
- data/lib/hecks/projections/deploy/scripts.rb +4 -57
- data/lib/hecks/projections/deploy/shared.rb +47 -146
- data/lib/hecks/projections/deploy/smoke.rb +4 -61
- data/lib/hecks/projections/deploy/template_diff/comparer.rb +3 -25
- data/lib/hecks/projections/deploy/template_diff/loader.rb +2 -11
- data/lib/hecks/projections/deploy/template_diff.rb +5 -57
- data/lib/hecks/projections/diagrams.rb +20 -582
- data/lib/hecks/projections/glossary/html.rb +14 -130
- data/lib/hecks/projections/glossary/markdown.rb +8 -69
- data/lib/hecks/projections/glossary/mermaid.rb +9 -66
- data/lib/hecks/projections/glossary/sensitivity.rb +5 -34
- data/lib/hecks/projections/glossary/sentences.rb +14 -159
- data/lib/hecks/projections/glossary.rb +30 -222
- data/lib/hecks/projections/ir.rb +3 -10
- data/lib/hecks/projections/model/deviations.rb +15 -42
- data/lib/hecks/projections/model.rb +12 -51
- data/lib/hecks/projections/oidc.rb +12 -56
- data/lib/hecks/projections/parser_table.rb +9 -28
- data/lib/hecks/projections/reference.rb +9 -16
- data/lib/hecks/projections/rust_vocabulary.rb +12 -250
- data/lib/hecks/projections/shape.rb +3 -29
- data/lib/hecks/projections/statements.rb +7 -86
- data/lib/hecks/projections/vocabulary.rb +6 -22
- data/lib/hecks/projections.rb +2 -16
- data/lib/hecks/projector/cli_projector.rb +60 -375
- data/lib/hecks/projector/docs_projector.rb +10 -201
- data/lib/hecks/projector/exporter.rb +46 -138
- data/lib/hecks/projector/ir_projector.rb +2 -9
- data/lib/hecks/projector/narrate_projector.rb +15 -189
- data/lib/hecks/projector/target.rb +13 -76
- data/lib/hecks/projector.rb +30 -121
- data/lib/hecks/query_specification/common/comparators.rb +5 -26
- data/lib/hecks/query_specification/common/comparison.rb +35 -143
- data/lib/hecks/query_specification/common/dsl.rb +8 -24
- data/lib/hecks/query_specification/common/null_policy.rb +20 -70
- data/lib/hecks/query_specification/common/options.rb +2 -6
- data/lib/hecks/query_specification/common/where_clause.rb +2 -10
- data/lib/hecks/query_specification/field_path.rb +37 -77
- data/lib/hecks/query_specification/hop_path.rb +12 -76
- data/lib/hecks/release/gem_pin.rb +2 -26
- data/lib/hecks/release/runner/ci_publisher.rb +93 -0
- data/lib/hecks/release/runner/commands.rb +66 -0
- data/lib/hecks/release/runner/console.rb +52 -0
- data/lib/hecks/release/runner/gem_publisher.rb +43 -0
- data/lib/hecks/release/runner/git.rb +48 -0
- data/lib/hecks/release/runner/npm_publisher.rb +75 -0
- data/lib/hecks/release/runner/preflight.rb +104 -0
- data/lib/hecks/release/runner/published.rb +55 -0
- data/lib/hecks/release/runner/tagger.rb +80 -0
- data/lib/hecks/release/runner.rb +215 -0
- data/lib/hecks/rendering.rb +4 -26
- data/lib/hecks/router/namespace_installer.rb +12 -36
- data/lib/hecks/router.rb +4 -8
- data/lib/hecks/runtime/aggregate_lock.rb +8 -33
- data/lib/hecks/runtime/boot_gates.rb +3 -18
- data/lib/hecks/runtime/caller.rb +19 -59
- data/lib/hecks/runtime/capability_graph.rb +5 -21
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +16 -78
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +23 -160
- data/lib/hecks/runtime/command_interpreter.rb +125 -307
- data/lib/hecks/runtime/command_rules/admissibility.rb +95 -366
- data/lib/hecks/runtime/command_rules/arithmetic.rb +66 -196
- data/lib/hecks/runtime/command_rules/authorization.rb +13 -45
- data/lib/hecks/runtime/command_rules/emission.rb +8 -21
- data/lib/hecks/runtime/command_rules/references.rb +23 -202
- data/lib/hecks/runtime/dependency_planning.rb +30 -102
- data/lib/hecks/runtime/dispatcher.rb +82 -241
- data/lib/hecks/runtime/entity_element.rb +40 -462
- data/lib/hecks/runtime/entity_interpreter.rb +65 -220
- data/lib/hecks/runtime/errors.rb +32 -115
- data/lib/hecks/runtime/event.rb +3 -25
- data/lib/hecks/runtime/identity.rb +18 -154
- data/lib/hecks/runtime/instance.rb +18 -150
- data/lib/hecks/runtime/interpreting.rb +11 -70
- data/lib/hecks/runtime/invocation.rb +28 -164
- data/lib/hecks/runtime/loader.rb +48 -186
- data/lib/hecks/runtime/outbox.rb +38 -261
- data/lib/hecks/runtime/policy_interpreter.rb +38 -200
- data/lib/hecks/runtime/port_operation_interpreter.rb +26 -87
- data/lib/hecks/runtime/query_interpreter.rb +45 -187
- data/lib/hecks/runtime/reaction_invocation.rb +36 -138
- data/lib/hecks/runtime/read_model_interpreter.rb +139 -208
- data/lib/hecks/runtime/rebuild_sweep.rb +8 -54
- data/lib/hecks/runtime/reference_hop.rb +8 -81
- data/lib/hecks/runtime/refusal_wording.rb +8 -89
- data/lib/hecks/runtime/registry/saga_persistence.rb +10 -102
- data/lib/hecks/runtime/registry/verification.rb +52 -225
- data/lib/hecks/runtime/registry/world_defaults.rb +4 -53
- data/lib/hecks/runtime/registry.rb +103 -244
- data/lib/hecks/runtime/remote_dispatcher.rb +37 -152
- data/lib/hecks/runtime/routing.rb +4 -15
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +14 -61
- data/lib/hecks/runtime/saga_interpreter.rb +79 -263
- data/lib/hecks/runtime/saga_pending_dispatch.rb +2 -43
- data/lib/hecks/runtime/tenant_check.rb +6 -68
- data/lib/hecks/runtime/tenant_scope.rb +3 -28
- data/lib/hecks/runtime/value/admission.rb +16 -67
- data/lib/hecks/runtime/value/coercion.rb +155 -641
- data/lib/hecks/runtime/value/entity_list_coercion.rb +26 -219
- data/lib/hecks/runtime/value.rb +12 -84
- data/lib/hecks/runtime.rb +17 -65
- data/lib/hecks/storehouse.rb +146 -567
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +8 -32
- data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +6 -23
- data/lib/hecks/vendoring/git_environment.rb +2 -8
- data/lib/hecks/vendoring/git_source.rb +4 -10
- data/lib/hecks/vendoring.rb +7 -46
- data/lib/hecks/version.rb +3 -14
- data/lib/hecks/vocabulary.rb +5 -0
- data/lib/hecks.rb +32 -82
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -46
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +11 -80
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +10 -99
- metadata +30 -59
- data/lib/hecks/bench/cli.rb +0 -74
- data/lib/hecks/bench/environment.rb +0 -100
- data/lib/hecks/bench/postgres_probe.rb +0 -41
- data/lib/hecks/bench/report.rb +0 -116
- data/lib/hecks/bench/ruby_runner.rb +0 -151
- data/lib/hecks/bench/run.rb +0 -55
- data/lib/hecks/bench/rust_runner.rb +0 -165
- data/lib/hecks/bench/stats.rb +0 -75
- data/lib/hecks/bench/suite.rb +0 -172
- data/lib/hecks/bench/workload.rb +0 -137
- data/lib/hecks/bench.rb +0 -28
- data/lib/hecks/codemod.rb +0 -443
- data/lib/hecks/corpus.rb +0 -479
- data/lib/hecks/doc/reference.rb +0 -585
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -684
- data/lib/hecks/fuzzing/combination_miner.rb +0 -178
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +0 -469
- data/lib/hecks/fuzzing/coverage_campaign.rb +0 -161
- data/lib/hecks/fuzzing/differential.rb +0 -192
- data/lib/hecks/fuzzing/domain_generator.rb +0 -871
- data/lib/hecks/fuzzing/era_boundary.rb +0 -154
- data/lib/hecks/fuzzing/form_census.rb +0 -285
- data/lib/hecks/fuzzing/generated_domain_check.rb +0 -171
- data/lib/hecks/fuzzing/invalid_value_generator.rb +0 -125
- data/lib/hecks/fuzzing/isolated_boot.rb +0 -658
- data/lib/hecks/fuzzing/nondeterministic.rb +0 -79
- data/lib/hecks/fuzzing/persistence_parity.rb +0 -253
- data/lib/hecks/fuzzing/properties/corrections.rb +0 -125
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +0 -688
- data/lib/hecks/fuzzing/properties/guards.rb +0 -398
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +0 -399
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +0 -137
- data/lib/hecks/fuzzing/properties/outbox.rb +0 -180
- data/lib/hecks/fuzzing/properties/querying.rb +0 -260
- data/lib/hecks/fuzzing/properties.rb +0 -267
- data/lib/hecks/fuzzing/qa_settings.rb +0 -164
- data/lib/hecks/fuzzing/replay.rb +0 -930
- data/lib/hecks/fuzzing/rotation_priority.rb +0 -114
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +0 -139
- data/lib/hecks/fuzzing/self_consistency.rb +0 -825
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +0 -532
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +0 -171
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +0 -127
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +0 -116
- data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +0 -138
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +0 -344
- data/lib/hecks/fuzzing/sequence_generator.rb +0 -311
- data/lib/hecks/fuzzing/shrinker.rb +0 -309
- data/lib/hecks/fuzzing/structural_skips.rb +0 -53
- data/lib/hecks/fuzzing/sweep_depth.rb +0 -61
- data/lib/hecks/fuzzing/target_capabilities.rb +0 -255
- data/lib/hecks/fuzzing/value_generator.rb +0 -340
- data/lib/hecks/fuzzing.rb +0 -23
- data/lib/hecks/grammar/evolve.rb +0 -524
- data/lib/hecks/query_ir.rb +0 -482
data/lib/hecks/storehouse.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
require "json"
|
|
2
2
|
require "fileutils"
|
|
3
3
|
require "time"
|
|
4
|
+
require_relative "cache_dir"
|
|
4
5
|
require_relative "facade/command_request"
|
|
5
6
|
require_relative "facade/json_door"
|
|
6
7
|
require_relative "naming"
|
|
@@ -8,127 +9,28 @@ require_relative "projector"
|
|
|
8
9
|
require_relative "runtime/errors"
|
|
9
10
|
|
|
10
11
|
module Hecks
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
# "Storehouse" the single highest-priority gap this repo had: "no
|
|
14
|
-
# per-command tool... the bluebook is the contract, the [door] just
|
|
15
|
-
# projects it." This module is that bus, borrowing its name too — the
|
|
16
|
-
# dispatch/query/state/... surface below is a pure function of a
|
|
17
|
-
# `Runtime::Dispatcher` plus plain Ruby arguments, no IO, no protocol
|
|
18
|
-
# awareness. `bin/hecks_mcp_door` is one projection of it — MCP over
|
|
19
|
-
# stdio — not the whole thing; a plain CLI door, an HTTP door, a second
|
|
20
|
-
# transport of any shape, would sit beside it on the exact same bus,
|
|
21
|
-
# sharing the same audit log and caller-identity handling, without
|
|
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
|
|
27
|
-
#
|
|
28
|
-
# catalog — what aggregates a domain declares, and what each can do
|
|
29
|
-
# describe — one aggregate's full command/query/refusal contract
|
|
30
|
-
# validate — is this domain's wiring sound (deep: also model-checks it)
|
|
31
|
-
# domains — every domain directory under a root, discovered not typed
|
|
32
|
-
# dispatch — issue a command (dry_run: preview, steps: batch)
|
|
33
|
-
# query — ask a question
|
|
34
|
-
# state — read what is actually stored, no verb involved
|
|
35
|
-
# events — what happened, with payloads, to one record — not just its
|
|
36
|
-
# current state; events this bus witnessed, sourced from
|
|
37
|
-
# its own audit log, not a full event-sourcing replay
|
|
38
|
-
# history — the full append-only journal, not just current state
|
|
39
|
-
# behaviors — run a domain's hand-curated `.behaviors` examples
|
|
40
|
-
# follow — tail this bus's own dispatch/query/state audit log
|
|
41
|
-
#
|
|
42
|
-
# ## Auditing and caller identity
|
|
43
|
-
#
|
|
44
|
-
# `dispatch`/`query`/`state` each take a `summary` — the survey's "every
|
|
45
|
-
# audit row carries human intent for free" — and `dispatch`/`query`
|
|
46
|
-
# additionally take an optional `source:` (`SOURCE_TAGS`, the survey's
|
|
47
|
-
# `SourceTag`: who is calling) and an optional `role:`/`actor_id:` —
|
|
48
|
-
# a real caller identity, bound for the call's duration via `Hecks.
|
|
49
|
-
# as_caller`, checked against a `role`-gated command's own declared
|
|
50
|
-
# role (`CommandRules::Authorization`) rather than merely documented by
|
|
51
|
-
# `describe`. `dispatch` requires it for any command that declares a
|
|
52
|
-
# role — `require_caller_for_role_gated!` refuses an unbound dispatch
|
|
53
|
-
# against one rather than silently running it unchecked; `query`'s own
|
|
54
|
-
# authorization runs on a separate mechanism (`Runtime::TenantScope`)
|
|
55
|
-
# that `role:` does not gate. This is self-asserted identity, not
|
|
56
|
-
# authentication: the caller states its own `role`/`actor_id`, and this
|
|
57
|
-
# bus checks it consistently once stated — see `bin/hecks_mcp_door`'s
|
|
58
|
-
# header for what that does and does not guarantee. Every call through
|
|
59
|
-
# those three, plus a dry run, is appended to a per-domain JSONL audit
|
|
60
|
-
# log (`record!`) `follow` tails back — a record of what the caller
|
|
61
|
-
# said it was, not independently verified identity. `catalog`/
|
|
62
|
-
# `describe`/`validate`/`domains`/`history`/`behaviors`/`events` need
|
|
63
|
-
# neither — they change nothing and commit nothing to any log.
|
|
64
|
-
#
|
|
65
|
-
# ## Booting is the caller's job
|
|
66
|
-
#
|
|
67
|
-
# A caller hands in an already-booted `runtime`, the same division of
|
|
68
|
-
# labor `Facade::CliRunner` already keeps against `bin/run`: booting a
|
|
69
|
-
# domain from a path is IO the calling `bin/` script owns, this stays a
|
|
70
|
-
# pure function of a `Runtime::Dispatcher` plus plain Ruby arguments —
|
|
71
|
-
# no different from `CliRunner.call(runtime:, argv:)` itself.
|
|
72
|
-
# `validate` and `domains` are the two exceptions: `validate`'s whole
|
|
73
|
-
# job is to attempt the boot and report whether it survived, so it
|
|
74
|
-
# takes the domain path instead and boots it; `domains` has no
|
|
75
|
-
# domain to be handed one of yet, that's what it's answering.
|
|
76
|
-
#
|
|
77
|
-
# ## Built from existing doors
|
|
78
|
-
#
|
|
79
|
-
# No new vocabulary otherwise. Every method here composes doors that
|
|
80
|
-
# already exist — `Projector.call(:cli, ...)` for verb/question alias
|
|
81
|
-
# resolution (the identical table `CliRunner` itself resolves against),
|
|
82
|
-
# `Projector.call(:docs, ...)` for `describe`, `Facade::JsonDoor` for
|
|
83
|
-
# the JSON↔Runtime::Value boundary, `Registry#repository` for `state`/
|
|
84
|
-
# `history`, `Registry#verify!` (run by every boot) for `validate`,
|
|
85
|
-
# `Bluebook::ModelCheck` for `validate(deep: true)`, `Hecks::Behaviors`
|
|
86
|
-
# for `behaviors`, `Adapters::Folder#domain?` for `domains`,
|
|
87
|
-
# `Dispatcher#dry_run?` for `dispatch(dry_run: true)`. A second copy of
|
|
88
|
-
# any of these here would be the exact duplication `Facade::JsonDoor`'s
|
|
89
|
-
# own header already warns against.
|
|
12
|
+
# Dispatch/query/state and introspection over one booted domain, pure
|
|
13
|
+
# beyond its own JSONL audit log — booting stays the caller's job.
|
|
90
14
|
module Storehouse
|
|
91
15
|
module_function
|
|
92
16
|
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
# that omits it gets `source: nil` recorded, honestly, rather than a
|
|
96
|
-
# guessed default.
|
|
17
|
+
# Optional on dispatch/query; omitted, source is recorded as nil rather
|
|
18
|
+
# than guessed.
|
|
97
19
|
SOURCE_TAGS = %w[process-manager operator hook sidequest-agent cascade daemon].freeze
|
|
98
20
|
|
|
99
|
-
#
|
|
100
|
-
#
|
|
101
|
-
#
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
#
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
# The root every `domain:`/`under:` must resolve under — the project
|
|
108
|
-
# directory by default, `HECKS_STOREHOUSE_ROOT` to widen or move it.
|
|
109
|
-
# `Hecks.boot` `Kernel.load`s the `.hecksagon`/`.bluebook`/`.world`
|
|
110
|
-
# files a domain path resolves to, and those are Ruby, not a data
|
|
111
|
-
# format — a caller-supplied path with no confinement at all is an
|
|
112
|
-
# unmarked door out of "a narrower, checked surface" (the README's
|
|
113
|
-
# own pitch for this bus) and into arbitrary code execution from any
|
|
114
|
-
# path on disk. This is a boundary a local convention was standing
|
|
115
|
-
# in for, not new behavior for a caller already confined to the
|
|
116
|
-
# project tree.
|
|
21
|
+
# Kept under Hecks::CacheDir, not the domain directory (part of the domain's
|
|
22
|
+
# own state) or the gem directory (read-only once installed).
|
|
23
|
+
# :nodoc:
|
|
24
|
+
def log_root = Hecks::CacheDir.path("storehouse")
|
|
25
|
+
|
|
26
|
+
# The root every domain path resolves under; HECKS_STOREHOUSE_ROOT widens it.
|
|
27
|
+
# Hecks.boot Kernel.loads Ruby files here, so an unconfined path is
|
|
28
|
+
# arbitrary code execution, not just a wrong-directory mistake.
|
|
117
29
|
BOOT_ROOT = File.expand_path(ENV["HECKS_STOREHOUSE_ROOT"] || File.expand_path("../..", __dir__))
|
|
118
30
|
|
|
119
|
-
#
|
|
120
|
-
#
|
|
121
|
-
#
|
|
122
|
-
# an honest mistake (a relative path typed against the wrong cwd) or
|
|
123
|
-
# the exact thing this check exists to catch, and both deserve the
|
|
124
|
-
# same clear refusal rather than a silent rewrite to something the
|
|
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`
|
|
31
|
+
# Refuses a path outside BOOT_ROOT rather than silently clamping it — a
|
|
32
|
+
# stray relative path and a deliberate escape both deserve a clear refusal.
|
|
33
|
+
# :nodoc:
|
|
132
34
|
def confine!(path, label)
|
|
133
35
|
resolved = File.expand_path(path.to_s, BOOT_ROOT)
|
|
134
36
|
return resolved if resolved == BOOT_ROOT || resolved.start_with?("#{BOOT_ROOT}#{File::SEPARATOR}")
|
|
@@ -138,45 +40,24 @@ module Hecks
|
|
|
138
40
|
"own root (HECKS_STOREHOUSE_ROOT to widen it)"
|
|
139
41
|
end
|
|
140
42
|
|
|
141
|
-
#
|
|
142
|
-
|
|
143
|
-
#
|
|
144
|
-
# projects a whole-domain CLI or doc set makes this same assumption
|
|
145
|
-
# (`Facade::CliRunner#call`'s own `bluebook = runtime.registry.
|
|
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
|
|
43
|
+
# The one bluebook this runtime booted; every whole-domain door assumes
|
|
44
|
+
# the same one-bluebook-per-boot shape.
|
|
45
|
+
# :nodoc:
|
|
152
46
|
def bluebook_for(runtime)
|
|
153
47
|
runtime.registry.bluebooks.values.first or
|
|
154
48
|
raise Runtime::NotFound, "this boot loaded no bluebook"
|
|
155
49
|
end
|
|
156
50
|
|
|
157
|
-
#
|
|
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`
|
|
51
|
+
# :nodoc:
|
|
163
52
|
def aggregate_ir!(bluebook, name)
|
|
164
53
|
bluebook.aggregate(name) or
|
|
165
54
|
raise Runtime::NotFound, "#{bluebook.name} declares no aggregate named #{name.inspect} — " \
|
|
166
55
|
"known: #{bluebook.aggregates.map(&:hecks_name).sort.join(', ')}"
|
|
167
56
|
end
|
|
168
57
|
|
|
169
|
-
# The same alias table
|
|
170
|
-
#
|
|
171
|
-
#
|
|
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
|
|
58
|
+
# The same alias table CliRunner resolves against, kept here so dispatch
|
|
59
|
+
# and query never drift from what a human typing bin/run sees.
|
|
60
|
+
# :nodoc:
|
|
180
61
|
def resolve!(cli, name, asking:)
|
|
181
62
|
pool = asking ? cli[:questions] : cli[:verbs]
|
|
182
63
|
key = cli[:names][asking ? :question : :command][name]
|
|
@@ -187,11 +68,8 @@ module Hecks
|
|
|
187
68
|
raise Runtime::NotFound, "no such #{asking ? 'query' : 'command'}: #{name.inspect} — known: #{known}"
|
|
188
69
|
end
|
|
189
70
|
|
|
190
|
-
#
|
|
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
|
|
71
|
+
# Required on dispatch/query/state: what makes an audit row legible later.
|
|
72
|
+
# :nodoc:
|
|
195
73
|
def require_summary!(summary)
|
|
196
74
|
return unless summary.nil? || summary.to_s.strip.empty?
|
|
197
75
|
|
|
@@ -199,80 +77,34 @@ module Hecks
|
|
|
199
77
|
"a one-line summary: is required on dispatch/query/state — it is what makes an audit row legible later"
|
|
200
78
|
end
|
|
201
79
|
|
|
202
|
-
#
|
|
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`
|
|
80
|
+
# :nodoc:
|
|
207
81
|
def valid_source!(source)
|
|
208
82
|
return if source.nil? || SOURCE_TAGS.include?(source.to_s)
|
|
209
83
|
|
|
210
84
|
raise Runtime::TypeMismatch, "source: #{source.inspect} is not one of #{SOURCE_TAGS.join(', ')}"
|
|
211
85
|
end
|
|
212
86
|
|
|
213
|
-
#
|
|
214
|
-
#
|
|
215
|
-
#
|
|
216
|
-
# `role` would silently do nothing rather than bind a real caller,
|
|
217
|
-
# which is worse than refusing: a caller who thinks they've
|
|
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`
|
|
87
|
+
# actor_id without role would silently bind nothing rather than a real
|
|
88
|
+
# caller — refusing here beats a caller thinking it identified itself.
|
|
89
|
+
# :nodoc:
|
|
224
90
|
def valid_caller!(role, actor_id)
|
|
225
91
|
return unless actor_id && role.nil?
|
|
226
92
|
|
|
227
93
|
raise Runtime::TypeMismatch, "actor_id: requires role: too — a caller names WHO through WHICH role they hold"
|
|
228
94
|
end
|
|
229
95
|
|
|
230
|
-
#
|
|
231
|
-
#
|
|
232
|
-
#
|
|
233
|
-
# for `query`, exactly as before: `CommandRules::Authorization#
|
|
234
|
-
# refuse_role_mismatch` is opt-in on the domain side (`return unless
|
|
235
|
-
# caller`), and query authorization runs on a wholly separate
|
|
236
|
-
# mechanism (`authorize policy, tenant: :field`, checked against an
|
|
237
|
-
# explicit `tenant:` argument — see `Runtime::TenantScope`), so
|
|
238
|
-
# binding a caller around a query has no effect on it today; it is
|
|
239
|
-
# still accepted here, for symmetry and for the audit log, against
|
|
240
|
-
# the day a read model does check `Caller.current`. For `dispatch`,
|
|
241
|
-
# `require_caller_for_role_gated!` (below) now refuses before this
|
|
242
|
-
# is ever reached when the command declares a role and no caller is
|
|
243
|
-
# bound — so an unbound `dispatch` here means either the command
|
|
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
|
|
96
|
+
# Binds role/actor_id for the block's duration via Hecks.as_caller; role: nil
|
|
97
|
+
# runs the block unbound (query's own authorization does not depend on it).
|
|
98
|
+
# :nodoc:
|
|
251
99
|
def with_caller(role, actor_id, &block)
|
|
252
100
|
return block.call if role.nil?
|
|
253
101
|
|
|
254
102
|
Hecks.as_caller(role: role, actor_id: actor_id, &block)
|
|
255
103
|
end
|
|
256
104
|
|
|
257
|
-
#
|
|
258
|
-
#
|
|
259
|
-
#
|
|
260
|
-
# string match), but changed nothing about a caller who binds no
|
|
261
|
-
# role at all: `refuse_role_mismatch` `return`s immediately when
|
|
262
|
-
# `Caller.current` is nil, so a bus caller who simply omits `role:`
|
|
263
|
-
# sails past a role-gated command unchecked, not denied. That is a
|
|
264
|
-
# property of this bus choosing to dispatch unbound, not of the
|
|
265
|
-
# domain rule — `bin/run`, the human CLI, has no such gap because a
|
|
266
|
-
# human always dispatches through a real `Hecks.as_caller` binding
|
|
267
|
-
# upstream of it. Refusing here, before `with_caller`/`dispatch` are
|
|
268
|
-
# ever reached, makes the bus keep the same promise: a command whose
|
|
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
|
|
105
|
+
# A caller who omits role: would otherwise reach a role-gated command
|
|
106
|
+
# unchecked (refuse_role_mismatch no-ops with no bound caller) — refuse here instead.
|
|
107
|
+
# :nodoc:
|
|
276
108
|
def require_caller_for_role_gated!(spec, role)
|
|
277
109
|
return unless spec[:role_gated] && role.nil?
|
|
278
110
|
|
|
@@ -281,26 +113,9 @@ module Hecks
|
|
|
281
113
|
"(role:/actor_id:) is bound; dispatching it unbound is refused, not silently unchecked"
|
|
282
114
|
end
|
|
283
115
|
|
|
284
|
-
#
|
|
285
|
-
#
|
|
286
|
-
#
|
|
287
|
-
# CommandInterpreter/EntityInterpreter's pre-envelope contract,
|
|
288
|
-
# never updated because nothing else needed it to be — a real
|
|
289
|
-
# record of history, not a defect this bus should paper over
|
|
290
|
-
# silently). `Facade::CommandRequest`/`spec[:legacy_receiver]`
|
|
291
|
-
# already know how to name that same flat shape for every receiver
|
|
292
|
-
# kind (a bare id under one string key for :aggregate, a
|
|
293
|
-
# {aggregate:, entity:} pair of keys for :entity) — this is the one
|
|
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
|
|
116
|
+
# dry_run? (Runtime::Dispatcher) only understands the pre-envelope flat
|
|
117
|
+
# args shape; this is the one door back into it from dispatch's own envelope.
|
|
118
|
+
# :nodoc:
|
|
304
119
|
def flatten_legacy(envelope, receiver, legacy_receiver)
|
|
305
120
|
facts = envelope[:with] || {}
|
|
306
121
|
return facts unless envelope.key?(:to)
|
|
@@ -315,69 +130,38 @@ module Hecks
|
|
|
315
130
|
end
|
|
316
131
|
end
|
|
317
132
|
|
|
318
|
-
#
|
|
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`
|
|
133
|
+
# :nodoc:
|
|
325
134
|
def log_path(domain_name)
|
|
326
|
-
File.join(
|
|
135
|
+
File.join(log_root, "#{domain_name.to_s.gsub(/[^A-Za-z0-9_-]/, '_')}.jsonl")
|
|
327
136
|
end
|
|
328
137
|
|
|
329
|
-
#
|
|
330
|
-
#
|
|
331
|
-
#
|
|
332
|
-
# written — a full disk or a permissions problem is a `follow`
|
|
333
|
-
# feature going dark, not a reason to refuse the dispatch/query/
|
|
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]
|
|
138
|
+
# Never fails a real call because its own audit log couldn't be written —
|
|
139
|
+
# a full disk silences follow, not the dispatch/query/state call itself.
|
|
140
|
+
# :nodoc:
|
|
348
141
|
def record!(domain_name, tool:, summary:, source:, outcome:, verb: nil, role: nil, actor_id: nil)
|
|
349
142
|
return unless domain_name
|
|
350
143
|
|
|
351
144
|
entry = { time: Time.now.utc.iso8601, tool: tool, verb: verb, summary: summary, source: source,
|
|
352
145
|
role: role, actor_id: actor_id,
|
|
353
146
|
ok: outcome[:ok], id: outcome[:id], error: outcome[:error], events: outcome[:events] }.compact
|
|
354
|
-
FileUtils.mkdir_p(
|
|
147
|
+
FileUtils.mkdir_p(log_root)
|
|
355
148
|
File.open(log_path(domain_name), "a") { |f| f.puts(JSON.generate(entry)) }
|
|
356
149
|
rescue StandardError
|
|
357
150
|
nil
|
|
358
151
|
end
|
|
359
152
|
|
|
360
|
-
#
|
|
361
|
-
|
|
362
|
-
# `dry_run: true` answers a different question than a real dispatch
|
|
363
|
-
# does — "would this succeed", not "here is what happened" — so a
|
|
364
|
-
# domain refusal is the legitimate, complete answer (`ok: true,
|
|
365
|
-
# would_succeed: false`), not a failed call. A malformed request
|
|
366
|
-
# (unknown command, a bad args shape) is still a failed call
|
|
367
|
-
# (`ok: false`) either way — it never reached the domain to be asked.
|
|
153
|
+
# Issues a command, or previews it with dry_run: true.
|
|
368
154
|
#
|
|
369
|
-
# @param runtime [Runtime::
|
|
155
|
+
# @param runtime [Runtime::Registry] the booted domain to dispatch against
|
|
370
156
|
# @param command [String, Symbol] the command name, bare or qualified
|
|
371
|
-
# @param summary [String] a one-line
|
|
372
|
-
# @param args [Hash] the command's arguments
|
|
373
|
-
# @param source [String, Symbol, nil] a
|
|
374
|
-
# @param dry_run [Boolean] true
|
|
375
|
-
#
|
|
376
|
-
# @param
|
|
377
|
-
# @
|
|
378
|
-
#
|
|
379
|
-
# (a real dispatch) or `:would_succeed`/`:error` (a dry run); on refusal, the
|
|
380
|
-
# shape `refused` returns
|
|
157
|
+
# @param summary [String] a one-line audit summary; required
|
|
158
|
+
# @param args [Hash] the command's arguments, JSON-shaped
|
|
159
|
+
# @param source [String, Symbol, nil] a SOURCE_TAGS tag naming who is calling
|
|
160
|
+
# @param dry_run [Boolean] true previews; a false would_succeed is not a failure
|
|
161
|
+
# @param role [String, Symbol, nil] the caller's bound role, or nil to run unbound
|
|
162
|
+
# @param actor_id [String, nil] the caller's identity; requires role:
|
|
163
|
+
# @return [Hash] :ok plus :id/:state/:events (real) or :would_succeed/:error
|
|
164
|
+
# (dry run); or the refused shape
|
|
381
165
|
def dispatch(runtime:, command:, summary:, args: {}, source: nil, dry_run: false, role: nil, actor_id: nil)
|
|
382
166
|
bluebook = bluebook_for(runtime)
|
|
383
167
|
tool = dry_run ? "dry_run" : "dispatch"
|
|
@@ -393,24 +177,7 @@ module Hecks
|
|
|
393
177
|
outcome
|
|
394
178
|
end
|
|
395
179
|
|
|
396
|
-
#
|
|
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
|
|
180
|
+
# :nodoc:
|
|
414
181
|
def perform_dispatch(runtime, bluebook, command, summary, args, source, dry_run, role, actor_id)
|
|
415
182
|
require_summary!(summary)
|
|
416
183
|
valid_source!(source)
|
|
@@ -428,14 +195,7 @@ module Hecks
|
|
|
428
195
|
result.merge(verb: spec[:verb])
|
|
429
196
|
end
|
|
430
197
|
|
|
431
|
-
#
|
|
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)
|
|
198
|
+
# :nodoc:
|
|
439
199
|
def real_dispatch(runtime, spec, envelope, summary)
|
|
440
200
|
result = runtime.dispatch_flat(spec[:verb], envelope)
|
|
441
201
|
ok(summary: summary,
|
|
@@ -444,14 +204,7 @@ module Hecks
|
|
|
444
204
|
events: result.events.map { |event| { name: event.name, payload: Facade::JsonDoor.materialize(event.payload) } })
|
|
445
205
|
end
|
|
446
206
|
|
|
447
|
-
#
|
|
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
|
|
207
|
+
# :nodoc:
|
|
455
208
|
def dry_run_outcome(runtime, spec, envelope, summary:)
|
|
456
209
|
flat = flatten_legacy(envelope, spec[:receiver], spec[:legacy_receiver])
|
|
457
210
|
runtime.dry_run?(spec[:verb], **flat)
|
|
@@ -460,28 +213,18 @@ module Hecks
|
|
|
460
213
|
ok(summary: summary, would_succeed: false, error: e.message)
|
|
461
214
|
end
|
|
462
215
|
|
|
463
|
-
# Dispatches a
|
|
464
|
-
#
|
|
465
|
-
#
|
|
466
|
-
#
|
|
467
|
-
# account, then fund it) pays one round trip instead of N. Every step
|
|
468
|
-
# goes through `dispatch` itself — same resolution, same audit log
|
|
469
|
-
# line per step, same summary/source stamped on all of them since
|
|
470
|
-
# the batch is what the caller is declaring intent about, not each
|
|
471
|
-
# individual step. Runs every step regardless of an earlier one
|
|
472
|
-
# refusing — a later step naming a record an earlier step never
|
|
473
|
-
# created will refuse honestly on its own account, which is more
|
|
474
|
-
# informative than silently dropping the rest of the batch.
|
|
216
|
+
# Dispatches a sequence of commands as one call, through dispatch itself —
|
|
217
|
+
# same audit log line per step. Runs every step even after an earlier one
|
|
218
|
+
# refuses, since a later step naming a since-refused record will refuse
|
|
219
|
+
# honestly on its own account.
|
|
475
220
|
#
|
|
476
|
-
# @param runtime [Runtime::
|
|
477
|
-
# @param steps [Array<Hash>] each step's
|
|
478
|
-
# @param summary [String] a one-line
|
|
479
|
-
# @param source [String, Symbol, nil] a
|
|
480
|
-
# @param role [String, Symbol, nil] the caller's
|
|
481
|
-
# @param actor_id [String, nil] the caller's
|
|
482
|
-
# @return [Hash
|
|
483
|
-
# true), `:summary` and `:results` (each step's own `dispatch` outcome); on a
|
|
484
|
-
# refusal outside any one step, the shape `refused` returns
|
|
221
|
+
# @param runtime [Runtime::Registry] the booted domain to dispatch against
|
|
222
|
+
# @param steps [Array<Hash>] each step's command/args, JSON-shaped
|
|
223
|
+
# @param summary [String] a one-line audit summary; required
|
|
224
|
+
# @param source [String, Symbol, nil] a SOURCE_TAGS tag naming who is calling
|
|
225
|
+
# @param role [String, Symbol, nil] the caller's bound role, or nil to run unbound
|
|
226
|
+
# @param actor_id [String, nil] the caller's identity; requires role:
|
|
227
|
+
# @return [Hash] :ok (true only if every step's own :ok was true), :results
|
|
485
228
|
def dispatch_batch(runtime:, steps:, summary:, source: nil, role: nil, actor_id: nil)
|
|
486
229
|
require_summary!(summary)
|
|
487
230
|
results = Array(steps).map do |raw|
|
|
@@ -496,15 +239,14 @@ module Hecks
|
|
|
496
239
|
|
|
497
240
|
# Answers one declared query.
|
|
498
241
|
#
|
|
499
|
-
# @param runtime [Runtime::
|
|
242
|
+
# @param runtime [Runtime::Registry] the booted domain to query
|
|
500
243
|
# @param question [String, Symbol] the query name, bare or qualified
|
|
501
|
-
# @param summary [String] a one-line
|
|
502
|
-
# @param args [Hash] the query's arguments
|
|
503
|
-
# @param source [String, Symbol, nil] a
|
|
504
|
-
# @param role [String, Symbol, nil] the caller's
|
|
505
|
-
# @param actor_id [String, nil] the caller's
|
|
506
|
-
# @return [Hash
|
|
507
|
-
# or the shape `refused` returns
|
|
244
|
+
# @param summary [String] a one-line audit summary; required
|
|
245
|
+
# @param args [Hash] the query's arguments, JSON-shaped
|
|
246
|
+
# @param source [String, Symbol, nil] a SOURCE_TAGS tag naming who is calling
|
|
247
|
+
# @param role [String, Symbol, nil] the caller's bound role, or nil to run unbound
|
|
248
|
+
# @param actor_id [String, nil] the caller's identity; requires role:
|
|
249
|
+
# @return [Hash] :ok and :rows (each JSON-safe); or the refused shape
|
|
508
250
|
def query(runtime:, question:, summary:, args: {}, source: nil, role: nil, actor_id: nil)
|
|
509
251
|
bluebook = bluebook_for(runtime)
|
|
510
252
|
outcome = perform_query(bluebook, runtime, question, summary, args, source, role, actor_id)
|
|
@@ -519,20 +261,7 @@ module Hecks
|
|
|
519
261
|
outcome
|
|
520
262
|
end
|
|
521
263
|
|
|
522
|
-
#
|
|
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
|
|
264
|
+
# :nodoc:
|
|
536
265
|
def perform_query(bluebook, runtime, question, summary, args, source, role, actor_id)
|
|
537
266
|
require_summary!(summary)
|
|
538
267
|
valid_source!(source)
|
|
@@ -544,22 +273,15 @@ module Hecks
|
|
|
544
273
|
ok(summary: summary, rows: rows.map { |row| Facade::JsonDoor.materialize(row) }).merge(verb: spec[:verb])
|
|
545
274
|
end
|
|
546
275
|
|
|
547
|
-
# Reads one aggregate's stored records directly, bypassing any declared
|
|
276
|
+
# Reads one aggregate's stored records directly, bypassing any declared
|
|
277
|
+
# query — id: answers one record, omitted answers every record.
|
|
548
278
|
#
|
|
549
|
-
#
|
|
550
|
-
# itself. `id:` given answers one record (`NotFound` when it names
|
|
551
|
-
# nothing); omitted answers every record the aggregate currently
|
|
552
|
-
# holds. This is the difference `query` can't cover: a query answers a
|
|
553
|
-
# declared question, and an aggregate that never declared "list
|
|
554
|
-
# everything" has no query this could reuse.
|
|
555
|
-
#
|
|
556
|
-
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
|
|
279
|
+
# @param runtime [Runtime::Registry] the booted domain to read
|
|
557
280
|
# @param aggregate [String, Symbol] the aggregate's declared name
|
|
558
|
-
# @param summary [String] a one-line
|
|
281
|
+
# @param summary [String] a one-line audit summary; required
|
|
559
282
|
# @param id [String, Object, nil] one record's identity, or nil for every record
|
|
560
|
-
# @param source [String, Symbol, nil] a
|
|
561
|
-
# @return [Hash
|
|
562
|
-
# `:count` and `:records` (each JSON-safe); or the shape `refused` returns
|
|
283
|
+
# @param source [String, Symbol, nil] a SOURCE_TAGS tag naming who is calling
|
|
284
|
+
# @return [Hash] :ok and, with id:, :record; without it, :count/:records; or refused
|
|
563
285
|
def state(runtime:, aggregate:, summary:, id: nil, source: nil)
|
|
564
286
|
bluebook = bluebook_for(runtime)
|
|
565
287
|
outcome = perform_state(runtime, bluebook, aggregate, summary, id)
|
|
@@ -572,18 +294,7 @@ module Hecks
|
|
|
572
294
|
outcome
|
|
573
295
|
end
|
|
574
296
|
|
|
575
|
-
#
|
|
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
|
|
297
|
+
# :nodoc:
|
|
587
298
|
def perform_state(runtime, bluebook, aggregate, summary, id)
|
|
588
299
|
require_summary!(summary)
|
|
589
300
|
ir = aggregate_ir!(bluebook, aggregate)
|
|
@@ -599,23 +310,12 @@ module Hecks
|
|
|
599
310
|
end
|
|
600
311
|
end
|
|
601
312
|
|
|
602
|
-
#
|
|
603
|
-
|
|
604
|
-
# Lists every domain directory under a root.
|
|
605
|
-
#
|
|
606
|
-
# **Zoom level zero** — every domain directory a root actually holds,
|
|
607
|
-
# discovered rather than typed from memory. Every other tool takes
|
|
608
|
-
# `domain:` as a directory it assumes the caller already knows; this
|
|
609
|
-
# is how a caller who doesn't finds out. `Adapters::Folder#domain?`
|
|
610
|
-
# is the same predicate `domain_root`/`nearest_domain` already walk
|
|
611
|
-
# up directories checking — a bare `.hecksagon` or one under
|
|
612
|
-
# `bluebook/`, the two real shapes this corpus uses.
|
|
313
|
+
# Lists every domain directory found under a root (discovered, not typed).
|
|
613
314
|
#
|
|
614
|
-
# @param under [String] the directory to search, relative to
|
|
615
|
-
# @return [Hash
|
|
616
|
-
#
|
|
617
|
-
#
|
|
618
|
-
# @raise [Runtime::TypeMismatch] if `under` resolves outside `BOOT_ROOT`
|
|
315
|
+
# @param under [String] the directory to search, relative to BOOT_ROOT
|
|
316
|
+
# @return [Hash] :ok, :under, :domains (each path relative to BOOT_ROOT,
|
|
317
|
+
# sorted); :domains is [] when under does not exist
|
|
318
|
+
# @raise [Runtime::TypeMismatch] if under resolves outside BOOT_ROOT
|
|
619
319
|
def domains(under: "examples")
|
|
620
320
|
root = confine!(under, "under")
|
|
621
321
|
return ok(under: under, domains: []) unless Dir.exist?(root)
|
|
@@ -626,17 +326,12 @@ module Hecks
|
|
|
626
326
|
ok(under: under, domains: found.map { |name| File.join(under, name) })
|
|
627
327
|
end
|
|
628
328
|
|
|
629
|
-
# Lists a domain's aggregates and their command/query names
|
|
329
|
+
# Lists a domain's aggregates and their command/query names, snake_cased
|
|
330
|
+
# exactly as dispatch/query want them.
|
|
630
331
|
#
|
|
631
|
-
#
|
|
632
|
-
#
|
|
633
|
-
#
|
|
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
|
|
332
|
+
# @param runtime [Runtime::Registry] the booted domain to describe
|
|
333
|
+
# @return [Hash] :ok, :domain, :aggregates (each {name:, commands:, queries:},
|
|
334
|
+
# command names suffixed !); or the refused shape
|
|
640
335
|
def catalog(runtime:)
|
|
641
336
|
bluebook = bluebook_for(runtime)
|
|
642
337
|
|
|
@@ -650,21 +345,14 @@ module Hecks
|
|
|
650
345
|
refused(e)
|
|
651
346
|
end
|
|
652
347
|
|
|
653
|
-
#
|
|
654
|
-
#
|
|
655
|
-
# **Zoom level two** — the exact same usage document a human gets from
|
|
656
|
-
# `bin/docs <domain> [aggregate]` (`Projector::DocsProjector`, the
|
|
657
|
-
# identical projection `Surface::AggregateDoor#docs` calls one door
|
|
658
|
-
# over): every command's arguments, the states it may be issued
|
|
659
|
-
# from, and every way it can refuse. `aggregate:` omitted answers the
|
|
660
|
-
# whole chapter.
|
|
348
|
+
# One aggregate's (or the whole chapter's) full usage documentation — the
|
|
349
|
+
# same document bin/docs renders for a human.
|
|
661
350
|
#
|
|
662
|
-
# @param runtime [Runtime::
|
|
351
|
+
# @param runtime [Runtime::Registry] the booted domain to describe
|
|
663
352
|
# @param aggregate [String, Symbol, nil] one aggregate's name, or nil for the
|
|
664
353
|
# whole chapter
|
|
665
|
-
# @return [Hash
|
|
666
|
-
#
|
|
667
|
-
# @raise [Runtime::NotFound] if `aggregate` names no known aggregate
|
|
354
|
+
# @return [Hash] :ok, :domain, :docs; or the refused shape
|
|
355
|
+
# @raise [Runtime::NotFound] if aggregate names no known aggregate
|
|
668
356
|
def describe(runtime:, aggregate: nil)
|
|
669
357
|
bluebook = bluebook_for(runtime)
|
|
670
358
|
options = aggregate ? { aggregate: aggregate_ir!(bluebook, aggregate).hecks_name } : {}
|
|
@@ -674,39 +362,16 @@ module Hecks
|
|
|
674
362
|
refused(e)
|
|
675
363
|
end
|
|
676
364
|
|
|
677
|
-
# Boots a domain and reports whether its wiring
|
|
678
|
-
#
|
|
679
|
-
#
|
|
680
|
-
#
|
|
681
|
-
#
|
|
682
|
-
# verification.rb`) is the one place this repo already answers that
|
|
683
|
-
# question, and `Runtime::Loader.boot` already calls it as the last
|
|
684
|
-
# step of every boot — so this is the one method here that boots for
|
|
685
|
-
# itself rather than taking a `runtime:` already in hand, because a
|
|
686
|
-
# runtime that successfully reached this line already answered the
|
|
687
|
-
# question. Given a domain path, not a booted runtime, deliberately:
|
|
688
|
-
# asking "is this valid" about a domain that failed to boot at all
|
|
689
|
-
# has to be askable without a runtime to hand it.
|
|
365
|
+
# Boots a domain and reports whether its wiring is sound; deep: true also
|
|
366
|
+
# runs Bluebook::ModelCheck past wiring, into logic. Boots for itself,
|
|
367
|
+
# unlike every other tool here, since a domain that failed to boot has
|
|
368
|
+
# no runtime to hand in — any boot failure answers the question, not just
|
|
369
|
+
# WiringError.
|
|
690
370
|
#
|
|
691
|
-
#
|
|
692
|
-
#
|
|
693
|
-
#
|
|
694
|
-
#
|
|
695
|
-
# "this cannot run at all", a model-check finding is "this runs, but
|
|
696
|
-
# part of it can never fire" — two different questions, and the
|
|
697
|
-
# first is far cheaper to ask on every boot.
|
|
698
|
-
#
|
|
699
|
-
# Any boot failure answers the question, not only `WiringError` — a
|
|
700
|
-
# domain path with no `.hecksagon`, a malformed bluebook, is just as
|
|
701
|
-
# much "not valid" as a real wiring mismatch, and this tool exists
|
|
702
|
-
# precisely so none of those ever cross a projection of this bus as
|
|
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
|
|
371
|
+
# @param domain [String] the domain's directory path, relative to BOOT_ROOT
|
|
372
|
+
# @param deep [Boolean] also run Bluebook::ModelCheck past wiring, into logic
|
|
373
|
+
# @return [Hash] {ok: true, domain:, valid: true} plus :findings when deep;
|
|
374
|
+
# {ok: false, domain:, valid: false, error:} if the boot itself failed
|
|
710
375
|
def validate(domain:, deep: false)
|
|
711
376
|
runtime = Hecks.boot(confine!(domain, "domain"), install_facade: false)
|
|
712
377
|
result = { ok: true, domain: domain, valid: true }
|
|
@@ -722,22 +387,12 @@ module Hecks
|
|
|
722
387
|
{ ok: false, domain: domain, valid: false, error: "#{e.class}: #{e.message}" }
|
|
723
388
|
end
|
|
724
389
|
|
|
725
|
-
#
|
|
726
|
-
|
|
727
|
-
# The full write history, not just the current head — `bin/history`'s
|
|
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.
|
|
390
|
+
# The full write history, not just the current head — every operation that
|
|
391
|
+
# ever touched each aggregate, off its repository's own append-only entries.
|
|
736
392
|
#
|
|
737
|
-
# @param runtime [Runtime::
|
|
738
|
-
# @return [Hash
|
|
739
|
-
#
|
|
740
|
-
# `refused` returns
|
|
393
|
+
# @param runtime [Runtime::Registry] the booted domain to read
|
|
394
|
+
# @return [Hash] :ok, :domain, :history (each aggregate's storage name
|
|
395
|
+
# mapped to its journal_entries); or the refused shape
|
|
741
396
|
def history(runtime:)
|
|
742
397
|
bluebook = bluebook_for(runtime)
|
|
743
398
|
entries = bluebook.aggregates.each_with_object({}) do |aggregate, all|
|
|
@@ -750,30 +405,17 @@ module Hecks
|
|
|
750
405
|
refused(e)
|
|
751
406
|
end
|
|
752
407
|
|
|
753
|
-
#
|
|
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
|
|
408
|
+
# :nodoc:
|
|
759
409
|
def journal_entries(repository)
|
|
760
410
|
repository.entries.map { |entry| { operation: entry.operation, id: entry.id, state: Facade::JsonDoor.materialize(entry.state) } }
|
|
761
411
|
end
|
|
762
412
|
|
|
763
|
-
#
|
|
764
|
-
# to use a domain, in domain vocabulary (`docs/guides/behaviors.md`),
|
|
765
|
-
# the survey's own "honest-refusal", generated-example-suite items.
|
|
766
|
-
# `Hecks::Behaviors` boots each test fresh through `Hecks.boot_files`
|
|
767
|
-
# itself — `target:` names a `.behaviors` file or a directory to
|
|
768
|
-
# sweep, never a `runtime:`, the one other method here besides
|
|
769
|
-
# `validate` that takes a path instead.
|
|
413
|
+
# Runs a domain's hand-curated .behaviors examples and reports the results.
|
|
770
414
|
#
|
|
771
|
-
# @param target [String] a
|
|
772
|
-
#
|
|
773
|
-
#
|
|
774
|
-
#
|
|
775
|
-
# the shape `refused` returns
|
|
776
|
-
# @raise [Runtime::NotFound] if `target` is nil or names no real file or directory
|
|
415
|
+
# @param target [String] a .behaviors file's path, or a directory to sweep
|
|
416
|
+
# @return [Hash] :ok, :target, :files (each file's behaviors_file shape),
|
|
417
|
+
# :counts; or the refused shape
|
|
418
|
+
# @raise [Runtime::NotFound] if target is nil or names no real file or directory
|
|
777
419
|
def behaviors(target:)
|
|
778
420
|
require_relative "behaviors"
|
|
779
421
|
raise Runtime::NotFound, "no such file or directory: #{target.inspect}" unless target && File.exist?(target)
|
|
@@ -789,31 +431,20 @@ module Hecks
|
|
|
789
431
|
refused(e)
|
|
790
432
|
end
|
|
791
433
|
|
|
792
|
-
#
|
|
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`)
|
|
434
|
+
# :nodoc:
|
|
797
435
|
def behaviors_file(result)
|
|
798
436
|
{ path: result.path,
|
|
799
437
|
parse_error: result.parse_error,
|
|
800
438
|
runs: Array(result.runs).map { |run| { description: run.description, status: run.status, message: run.message } } }
|
|
801
439
|
end
|
|
802
440
|
|
|
803
|
-
#
|
|
804
|
-
#
|
|
805
|
-
#
|
|
806
|
-
# of the survey's `storehouse follow` for that shape: not a
|
|
807
|
-
# subscription, a durable JSONL log every `dispatch`/`query`/`state`/
|
|
808
|
-
# dry-run call appends to (`record!`), tailed back here. Still real,
|
|
809
|
-
# still cross-process — the log outlives any one door's own process —
|
|
810
|
-
# just pull instead of push.
|
|
441
|
+
# Tails this bus's own dispatch/query/state audit log — a pull-based
|
|
442
|
+
# substitute for a push subscription, since a stdio door answers one
|
|
443
|
+
# request at a time with no channel to push through.
|
|
811
444
|
#
|
|
812
|
-
# @param runtime [Runtime::
|
|
813
|
-
# @param limit [Integer, #to_i] how many
|
|
814
|
-
#
|
|
815
|
-
# @return [Hash{Symbol => Object}] `:ok`, `:domain` and `:entries` (each logged
|
|
816
|
-
# call's own JSON entry); or the shape `refused` returns
|
|
445
|
+
# @param runtime [Runtime::Registry] the booted domain to tail
|
|
446
|
+
# @param limit [Integer, #to_i] how many recent log entries to return
|
|
447
|
+
# @return [Hash] :ok, :domain, :entries; or the refused shape
|
|
817
448
|
def follow(runtime:, limit: 20)
|
|
818
449
|
bluebook = bluebook_for(runtime)
|
|
819
450
|
entries = log_lines(bluebook.name).last([limit.to_i, 1].max)
|
|
@@ -823,11 +454,7 @@ module Hecks
|
|
|
823
454
|
refused(e)
|
|
824
455
|
end
|
|
825
456
|
|
|
826
|
-
#
|
|
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
|
|
457
|
+
# :nodoc:
|
|
831
458
|
def log_lines(domain_name)
|
|
832
459
|
path = log_path(domain_name)
|
|
833
460
|
return [] unless File.exist?(path)
|
|
@@ -835,32 +462,18 @@ module Hecks
|
|
|
835
462
|
File.readlines(path).map { |line| JSON.parse(line, symbolize_names: true) }
|
|
836
463
|
end
|
|
837
464
|
|
|
838
|
-
# What actually happened, with payloads
|
|
839
|
-
# (
|
|
840
|
-
#
|
|
841
|
-
#
|
|
842
|
-
# during the very call that populated it, discarded the moment that
|
|
843
|
-
# call returns — there is no cross-call in-memory log to read here.
|
|
844
|
-
# So this reads the same durable audit log `follow` already tails
|
|
845
|
-
# (`record!` now stamps a successful dispatch's own announced
|
|
846
|
-
# events onto its log line), reshaped: `follow` answers "what was
|
|
847
|
-
# called, in order, across every tool"; this answers "what happened
|
|
848
|
-
# to one aggregate/record" — events this bus witnessed, which is
|
|
849
|
-
# every real dispatch ever routed through it, but no more than that.
|
|
850
|
-
# `aggregate:` narrows to one aggregate; `id:` (requires
|
|
851
|
-
# `aggregate:` — an id alone is not unique across aggregates)
|
|
852
|
-
# narrows to one record's own events.
|
|
465
|
+
# What actually happened, with payloads, to one aggregate or record — read
|
|
466
|
+
# off this bus's own audit log (a dispatch's announced events), not a full
|
|
467
|
+
# event-sourcing replay. aggregate: narrows the search; id: (requires
|
|
468
|
+
# aggregate:) narrows further to one record.
|
|
853
469
|
#
|
|
854
|
-
# @param runtime [Runtime::
|
|
855
|
-
# @param aggregate [String, Symbol, nil] narrows to one aggregate
|
|
856
|
-
# @param id [String, Object, nil] narrows to one record
|
|
857
|
-
#
|
|
858
|
-
#
|
|
859
|
-
#
|
|
860
|
-
# @
|
|
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
|
|
470
|
+
# @param runtime [Runtime::Registry] the booted domain to search
|
|
471
|
+
# @param aggregate [String, Symbol, nil] narrows the search to one aggregate
|
|
472
|
+
# @param id [String, Object, nil] narrows further to one record; requires aggregate
|
|
473
|
+
# @param limit [Integer, #to_i, nil] how many recent matching events to
|
|
474
|
+
# return; nil returns every one found
|
|
475
|
+
# @return [Hash] :ok, :domain, :events; or the refused shape
|
|
476
|
+
# @raise [Runtime::TypeMismatch] if id is given without aggregate
|
|
864
477
|
def events(runtime:, aggregate: nil, id: nil, limit: nil)
|
|
865
478
|
raise Runtime::TypeMismatch, "id: requires aggregate: too — an id alone is not unique across aggregates" if id && !aggregate
|
|
866
479
|
|
|
@@ -875,20 +488,7 @@ module Hecks
|
|
|
875
488
|
refused(e)
|
|
876
489
|
end
|
|
877
490
|
|
|
878
|
-
#
|
|
879
|
-
# was a successful dispatch carrying events, matching `fqn` (when an
|
|
880
|
-
# aggregate narrowed the search) and `id` (when a record did). A
|
|
881
|
-
# self-contained per-entry check with nothing to share with its
|
|
882
|
-
# neighbors, extracted only to keep `events` itself to the query's
|
|
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
|
|
491
|
+
# :nodoc:
|
|
892
492
|
def entry_events(entry, fqn, id)
|
|
893
493
|
return unless entry[:tool] == "dispatch" && entry[:ok] && entry[:events]
|
|
894
494
|
return if fqn && !entry[:verb].to_s.start_with?("#{fqn}.")
|
|
@@ -897,39 +497,18 @@ module Hecks
|
|
|
897
497
|
entry[:events].map { |event| event.merge(time: entry[:time], verb: entry[:verb], id: entry[:id]) }
|
|
898
498
|
end
|
|
899
499
|
|
|
900
|
-
#
|
|
901
|
-
|
|
902
|
-
#
|
|
903
|
-
#
|
|
904
|
-
# rule the caller broke), but because "this domain isn't wired to
|
|
905
|
-
# answer what you're asking" (a `role:`+`actor_id:` caller reaching
|
|
906
|
-
# an authorization port nothing implements, a dry run against a
|
|
907
|
-
# port verb) is exactly the shape this bus promises never crashes
|
|
908
|
-
# through it. `dry_run_outcome` already treats it this way locally;
|
|
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
|
|
500
|
+
# Runtime::WiringError is included alongside the true domain refusals: a
|
|
501
|
+
# bus caller sees an honest refusal here too, not a crash, even though a
|
|
502
|
+
# wiring defect isn't a rule the caller broke.
|
|
503
|
+
# :nodoc:
|
|
913
504
|
def refusal_classes = [Runtime::NotFound, Runtime::TypeMismatch, Runtime::WiringError, *Runtime::DOMAIN_REFUSALS]
|
|
914
505
|
|
|
915
|
-
#
|
|
916
|
-
#
|
|
917
|
-
# @param fields [Hash{Symbol => Object}] fields to merge in alongside `ok: true`
|
|
918
|
-
# @return [Hash{Symbol => Object}] `{ok: true}` merged with `fields`
|
|
506
|
+
# :nodoc:
|
|
919
507
|
def ok(**fields) = { ok: true }.merge(fields)
|
|
920
508
|
|
|
921
|
-
#
|
|
922
|
-
#
|
|
923
|
-
#
|
|
924
|
-
# explicit, structured refusal a caller can act on" rather than a
|
|
925
|
-
# stack trace an agent has to parse to find the one line that
|
|
926
|
-
# mattered. The domain's own refusal text travels verbatim
|
|
927
|
-
# (`RefusalWording` already renders every one of these to be read),
|
|
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:}`
|
|
509
|
+
# The domain's own refusal text travels verbatim; this only wraps it
|
|
510
|
+
# consistently, rather than letting a stack trace reach the caller.
|
|
511
|
+
# :nodoc:
|
|
933
512
|
def refused(error, summary: nil) = { ok: false, summary: summary, error: error.message }
|
|
934
513
|
end
|
|
935
514
|
end
|