hecks 1.3.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 +72 -7
- data/lib/hecks/adapters/driven/d1.rb +187 -23
- data/lib/hecks/adapters/driven/folder.rb +83 -10
- data/lib/hecks/adapters/driven/google_authentication.rb +33 -12
- data/lib/hecks/adapters/driven/governance_authorization.rb +87 -18
- data/lib/hecks/adapters/driven/heki/journal.rb +13 -4
- data/lib/hecks/adapters/driven/heki/saga_store.rb +56 -10
- data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
- data/lib/hecks/adapters/driven/heki.rb +106 -9
- data/lib/hecks/adapters/driven/identity_registry.rb +12 -2
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +25 -3
- 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 +67 -14
- data/lib/hecks/adapters/driven/lambda.rb +82 -35
- data/lib/hecks/adapters/driven/local_storage.rb +83 -10
- data/lib/hecks/adapters/driven/memory.rb +205 -9
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +21 -1
- data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
- data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
- data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
- data/lib/hecks/adapters/driven/postgres.rb +175 -28
- data/lib/hecks/adapters/driven/prism.rb +50 -11
- data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
- data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
- data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
- data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
- data/lib/hecks/adapters/driven/sqlite.rb +181 -21
- 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 +6 -4
- data/lib/hecks/adapters/driving/github_webhook.rb +31 -18
- data/lib/hecks/behaviors/dsl.rb +60 -2
- data/lib/hecks/behaviors/expectations.rb +190 -29
- data/lib/hecks/behaviors/ir.rb +12 -1
- data/lib/hecks/behaviors/rspec.rb +9 -1
- data/lib/hecks/behaviors/runner.rb +21 -2
- data/lib/hecks/behaviors.rb +9 -1
- data/lib/hecks/bluebook/aggregate.rb +43 -13
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +17 -10
- data/lib/hecks/bluebook/assembly/build.rb +26 -5
- data/lib/hecks/bluebook/assembly/contract.rb +98 -23
- data/lib/hecks/bluebook/assembly/contracts.rb +59 -52
- data/lib/hecks/bluebook/assembly/marks.rb +159 -30
- data/lib/hecks/bluebook/assembly/specializer.rb +38 -21
- data/lib/hecks/bluebook/assembly.rb +32 -14
- data/lib/hecks/bluebook/attribute.rb +26 -12
- data/lib/hecks/bluebook/behaviour/aggregate.rb +43 -11
- data/lib/hecks/bluebook/behaviour/attribute.rb +18 -5
- data/lib/hecks/bluebook/behaviour/chapter.rb +76 -5
- data/lib/hecks/bluebook/behaviour/command.rb +55 -25
- data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -7
- data/lib/hecks/bluebook/behaviour/entity.rb +20 -8
- data/lib/hecks/bluebook/behaviour/hexagon.rb +30 -4
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +27 -6
- data/lib/hecks/bluebook/behaviour/policy.rb +42 -17
- data/lib/hecks/bluebook/behaviour/process_manager.rb +39 -8
- data/lib/hecks/bluebook/behaviour/query.rb +6 -1
- data/lib/hecks/bluebook/behaviour/read_model.rb +29 -8
- data/lib/hecks/bluebook/behaviour/traits.rb +48 -12
- data/lib/hecks/bluebook/behaviour/value_object.rb +21 -9
- data/lib/hecks/bluebook/capabilities.rb +27 -0
- data/lib/hecks/bluebook/chapter.rb +51 -9
- data/lib/hecks/bluebook/command.rb +62 -17
- data/lib/hecks/bluebook/domain_port.rb +34 -9
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +282 -123
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +112 -75
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +81 -2
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +486 -117
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +179 -47
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +284 -122
- data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
- data/lib/hecks/bluebook/dsl/entity_builder.rb +191 -61
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +130 -30
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +38 -21
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
- data/lib/hecks/bluebook/dsl/policy_builder.rb +86 -36
- data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +111 -47
- data/lib/hecks/bluebook/dsl/query_builder.rb +37 -8
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +127 -52
- data/lib/hecks/bluebook/dsl/rule_reference.rb +97 -43
- data/lib/hecks/bluebook/dsl/translation_builder.rb +150 -44
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +68 -20
- data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
- data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
- data/lib/hecks/bluebook/entity.rb +40 -11
- data/lib/hecks/bluebook/expression/ast_json.rb +128 -36
- data/lib/hecks/bluebook/expression/ast_reader.rb +32 -3
- data/lib/hecks/bluebook/expression/canonical_form.rb +55 -16
- data/lib/hecks/bluebook/expression/evaluator.rb +221 -43
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +54 -18
- data/lib/hecks/bluebook/expression/resolver.rb +369 -128
- data/lib/hecks/bluebook/hexagon.rb +35 -1
- data/lib/hecks/bluebook/lifecycle.rb +12 -1
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +2 -1
- data/lib/hecks/bluebook/meta_validator/judge.rb +126 -108
- data/lib/hecks/bluebook/meta_validator/plan.rb +81 -46
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +3 -2
- data/lib/hecks/bluebook/meta_validator/readings.rb +200 -50
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +68 -41
- data/lib/hecks/bluebook/meta_validator/shapes.rb +166 -21
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +286 -46
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +11 -10
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -5
- data/lib/hecks/bluebook/meta_validator.rb +235 -139
- data/lib/hecks/bluebook/model_check.rb +434 -104
- data/lib/hecks/bluebook/pattern_subset.rb +32 -10
- data/lib/hecks/bluebook/policy.rb +15 -13
- data/lib/hecks/bluebook/process_manager.rb +27 -14
- 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 +50 -6
- data/lib/hecks/bluebook/query.rb +31 -4
- data/lib/hecks/bluebook/read_model.rb +35 -15
- data/lib/hecks/bluebook/reference.rb +26 -13
- data/lib/hecks/bluebook/smoke_test.rb +46 -23
- data/lib/hecks/bluebook/synthesizer.rb +46 -12
- data/lib/hecks/bluebook/translation.rb +34 -5
- data/lib/hecks/bluebook/value_object.rb +29 -11
- data/lib/hecks/bluebook.rb +5 -6
- data/lib/hecks/codemod.rb +138 -50
- data/lib/hecks/construct.rb +21 -7
- data/lib/hecks/corpus.rb +438 -0
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
- data/lib/hecks/doc/reference.rb +200 -31
- data/lib/hecks/embryonaut_bluebook.rb +38 -15
- data/lib/hecks/facade/cli_door.rb +69 -10
- data/lib/hecks/facade/cli_runner.rb +105 -24
- data/lib/hecks/facade/command_request.rb +23 -0
- data/lib/hecks/facade/handle.rb +155 -35
- data/lib/hecks/facade/json_door.rb +106 -25
- data/lib/hecks/facade/surface/aggregate_door.rb +50 -27
- data/lib/hecks/facade/surface/chapter.rb +26 -17
- data/lib/hecks/facade/surface.rb +16 -3
- data/lib/hecks/facade.rb +15 -4
- data/lib/hecks/forms/app.rb +46 -30
- data/lib/hecks/forms/command_form_renderer.rb +70 -9
- data/lib/hecks/forms/field_renderer.rb +142 -6
- data/lib/hecks/forms/field_shape.rb +183 -19
- data/lib/hecks/forms/html.rb +51 -7
- data/lib/hecks/forms/index_renderer.rb +14 -2
- data/lib/hecks/forms/page.rb +14 -0
- data/lib/hecks/forms/params.rb +120 -23
- data/lib/hecks/forms/port_argument.rb +14 -2
- data/lib/hecks/forms/query_form_renderer.rb +65 -2
- data/lib/hecks/forms/record_renderer.rb +60 -2
- data/lib/hecks/forms/record_table.rb +28 -1
- data/lib/hecks/forms/reference_options.rb +24 -0
- data/lib/hecks/forms/value_object_shape.rb +13 -3
- data/lib/hecks/forms.rb +24 -4
- data/lib/hecks/fqn.rb +59 -1
- data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
- data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
- data/lib/hecks/framework/oidc.json +15 -0
- data/lib/hecks/framework.rb +79 -25
- data/lib/hecks/freezer.rb +27 -11
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +241 -106
- data/lib/hecks/fuzzing/combination_miner.rb +178 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +241 -45
- data/lib/hecks/fuzzing/coverage_campaign.rb +161 -0
- data/lib/hecks/fuzzing/differential.rb +192 -0
- data/lib/hecks/fuzzing/domain_generator.rb +871 -0
- data/lib/hecks/fuzzing/era_boundary.rb +68 -19
- data/lib/hecks/fuzzing/form_census.rb +121 -20
- data/lib/hecks/fuzzing/generated_domain_check.rb +171 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +45 -6
- data/lib/hecks/fuzzing/isolated_boot.rb +137 -80
- data/lib/hecks/fuzzing/nondeterministic.rb +79 -0
- data/lib/hecks/fuzzing/persistence_parity.rb +111 -21
- data/lib/hecks/fuzzing/properties/corrections.rb +34 -9
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +300 -51
- data/lib/hecks/fuzzing/properties/guards.rb +86 -42
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +80 -32
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +40 -35
- data/lib/hecks/fuzzing/properties/outbox.rb +70 -32
- data/lib/hecks/fuzzing/properties/querying.rb +82 -28
- data/lib/hecks/fuzzing/properties.rb +84 -51
- data/lib/hecks/fuzzing/qa_settings.rb +164 -0
- data/lib/hecks/fuzzing/replay.rb +241 -133
- data/lib/hecks/fuzzing/rotation_priority.rb +48 -28
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +139 -0
- data/lib/hecks/fuzzing/self_consistency.rb +315 -137
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +46 -40
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +59 -32
- data/lib/hecks/fuzzing/sequence_generator.rb +158 -23
- data/lib/hecks/fuzzing/shrinker.rb +309 -0
- data/lib/hecks/fuzzing/structural_skips.rb +37 -130
- data/lib/hecks/fuzzing/sweep_depth.rb +12 -4
- data/lib/hecks/fuzzing/target_capabilities.rb +107 -29
- data/lib/hecks/fuzzing/value_generator.rb +110 -22
- data/lib/hecks/fuzzing.rb +5 -0
- data/lib/hecks/grammar/evolve.rb +188 -12
- data/lib/hecks/grammar.rb +53 -7
- data/lib/hecks/ir.rb +51 -20
- data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
- data/lib/hecks/language/bluebook/policy.bluebook +11 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
- data/lib/hecks/language/oidc.json +5 -0
- data/lib/hecks/literal.rb +41 -9
- data/lib/hecks/naming.rb +112 -31
- data/lib/hecks/ports/access_control.rb +53 -2
- data/lib/hecks/ports/agent/answers.rb +83 -6
- data/lib/hecks/ports/agent.rb +119 -35
- data/lib/hecks/ports/authentication.rb +44 -4
- data/lib/hecks/ports/authorization.rb +50 -11
- data/lib/hecks/ports/clock.rb +42 -23
- data/lib/hecks/ports/extraction.rb +16 -0
- data/lib/hecks/ports/identity_assignment.rb +23 -2
- data/lib/hecks/ports/identity_generation.rb +17 -3
- data/lib/hecks/ports/identity_resolution.rb +17 -1
- data/lib/hecks/ports/key_vault.port +6 -0
- data/lib/hecks/ports/key_vault.rb +58 -0
- data/lib/hecks/ports/loading.rb +4 -0
- data/lib/hecks/ports/persistence/append_only.rb +172 -8
- data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
- data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
- data/lib/hecks/ports/persistence/execution.rb +4 -0
- data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
- data/lib/hecks/ports/persistence/plugin.rb +42 -4
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
- data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +34 -5
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +95 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
- data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +48 -8
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +58 -19
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +96 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +9 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
- data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
- data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
- data/lib/hecks/ports/persistence/state_codec.rb +319 -0
- data/lib/hecks/ports/persistence.rb +36 -1
- data/lib/hecks/ports/projection.rb +61 -7
- data/lib/hecks/ports/query/in_memory.rb +41 -8
- data/lib/hecks/ports/query/ordering.rb +21 -6
- data/lib/hecks/ports/query.rb +35 -0
- data/lib/hecks/ports.rb +1 -0
- data/lib/hecks/projections/bootstrap_table.rb +147 -0
- data/lib/hecks/projections/diagrams.rb +315 -79
- data/lib/hecks/projections/glossary/html.rb +91 -3
- data/lib/hecks/projections/glossary/markdown.rb +50 -3
- data/lib/hecks/projections/glossary/mermaid.rb +49 -1
- data/lib/hecks/projections/glossary/sections.rb +1 -1
- data/lib/hecks/projections/glossary/sentences.rb +110 -5
- data/lib/hecks/projections/glossary.rb +173 -31
- data/lib/hecks/projections/ir.rb +1 -1
- data/lib/hecks/projections/model/deviations.rb +62 -17
- data/lib/hecks/projections/model.rb +75 -21
- data/lib/hecks/projections/oidc.rb +45 -8
- data/lib/hecks/projections/parser_table.rb +53 -12
- data/lib/hecks/projections/reference.rb +15 -3
- data/lib/hecks/projections/rust_vocabulary.rb +646 -0
- data/lib/hecks/projections/shape.rb +10 -2
- data/lib/hecks/projections/statements.rb +69 -22
- data/lib/hecks/projections/vocabulary.rb +26 -9
- data/lib/hecks/projections.rb +5 -3
- data/lib/hecks/projector/cli_projector.rb +245 -37
- data/lib/hecks/projector/docs_projector.rb +154 -28
- data/lib/hecks/projector/exporter.rb +104 -29
- data/lib/hecks/projector/ir_projector.rb +7 -1
- data/lib/hecks/projector/narrate_projector.rb +144 -23
- data/lib/hecks/projector/target.rb +42 -18
- data/lib/hecks/projector.rb +86 -18
- data/lib/hecks/query_ir.rb +94 -47
- data/lib/hecks/query_specification/common/comparators.rb +19 -3
- data/lib/hecks/query_specification/common/comparison.rb +109 -27
- data/lib/hecks/query_specification/common/dsl.rb +65 -9
- data/lib/hecks/query_specification/common/null_policy.rb +61 -15
- data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
- data/lib/hecks/query_specification/common/options.rb +25 -0
- data/lib/hecks/query_specification/field_path.rb +69 -15
- data/lib/hecks/query_specification/hop_path.rb +57 -20
- data/lib/hecks/query_specification/read_model/specification.rb +4 -0
- data/lib/hecks/rendering.rb +9 -3
- data/lib/hecks/router/namespace_installer.rb +16 -3
- data/lib/hecks/router.rb +56 -1
- data/lib/hecks/runtime/aggregate_lock.rb +20 -11
- data/lib/hecks/runtime/boot_gates.rb +21 -3
- data/lib/hecks/runtime/caller.rb +40 -8
- data/lib/hecks/runtime/capability_graph.rb +13 -2
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +40 -41
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +45 -67
- data/lib/hecks/runtime/command_interpreter.rb +195 -113
- data/lib/hecks/runtime/command_rules/admissibility.rb +231 -80
- data/lib/hecks/runtime/command_rules/arithmetic.rb +154 -96
- data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
- data/lib/hecks/runtime/command_rules/emission.rb +18 -1
- data/lib/hecks/runtime/command_rules/references.rb +145 -55
- data/lib/hecks/runtime/command_rules.rb +3 -0
- data/lib/hecks/runtime/dependency_planning.rb +56 -11
- data/lib/hecks/runtime/dispatcher.rb +263 -92
- data/lib/hecks/runtime/entity_element.rb +304 -67
- data/lib/hecks/runtime/entity_interpreter.rb +149 -88
- data/lib/hecks/runtime/errors.rb +37 -23
- data/lib/hecks/runtime/event.rb +15 -10
- data/lib/hecks/runtime/identity.rb +91 -23
- data/lib/hecks/runtime/instance.rb +103 -18
- data/lib/hecks/runtime/interpreting.rb +24 -16
- data/lib/hecks/runtime/invocation.rb +358 -0
- data/lib/hecks/runtime/loader.rb +105 -19
- data/lib/hecks/runtime/outbox.rb +164 -26
- data/lib/hecks/runtime/policy_interpreter.rb +73 -60
- data/lib/hecks/runtime/port_operation_interpreter.rb +42 -19
- data/lib/hecks/runtime/query_interpreter.rb +93 -74
- data/lib/hecks/runtime/reaction_invocation.rb +73 -28
- data/lib/hecks/runtime/read_model_interpreter.rb +60 -44
- data/lib/hecks/runtime/rebuild_sweep.rb +32 -4
- data/lib/hecks/runtime/reference_hop.rb +48 -6
- data/lib/hecks/runtime/refusal_wording.rb +142 -115
- data/lib/hecks/runtime/registry/saga_persistence.rb +32 -21
- data/lib/hecks/runtime/registry/verification.rb +153 -28
- data/lib/hecks/runtime/registry.rb +202 -32
- data/lib/hecks/runtime/remote_dispatcher.rb +125 -24
- data/lib/hecks/runtime/routing.rb +36 -154
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +25 -27
- data/lib/hecks/runtime/saga_interpreter.rb +90 -76
- data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
- data/lib/hecks/runtime/tenant_check.rb +33 -13
- data/lib/hecks/runtime/tenant_scope.rb +23 -5
- data/lib/hecks/runtime/value/admission.rb +75 -30
- data/lib/hecks/runtime/value/coercion.rb +555 -142
- data/lib/hecks/runtime/value/entity_list_coercion.rb +132 -60
- data/lib/hecks/runtime/value.rb +71 -21
- data/lib/hecks/runtime.rb +39 -7
- data/lib/hecks/storehouse.rb +368 -72
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
- data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
- data/lib/hecks/version.rb +3 -3
- data/lib/hecks/vocabulary.rb +205 -4
- data/lib/hecks.rb +91 -11
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +19 -11
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +24 -12
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +38 -14
- metadata +28 -2
data/lib/hecks/behaviors/dsl.rb
CHANGED
|
@@ -16,6 +16,7 @@ module Hecks
|
|
|
16
16
|
# `validate_expect!` (private, below) is where a malformed or empty
|
|
17
17
|
# `expect` is refused at build time rather than silently passing later.
|
|
18
18
|
class TestCaseBuilder
|
|
19
|
+
# @param description [String] the `test "description" do ... end` text
|
|
19
20
|
def initialize(description)
|
|
20
21
|
@description = description
|
|
21
22
|
@tests_command = nil
|
|
@@ -26,19 +27,46 @@ module Hecks
|
|
|
26
27
|
@expect = {}
|
|
27
28
|
end
|
|
28
29
|
|
|
30
|
+
# Records which command or query this test exercises.
|
|
31
|
+
#
|
|
32
|
+
# @param command [String, Symbol] the command or query verb, bare or dotted FQN
|
|
33
|
+
# @param on [String, Symbol, nil] the aggregate `command` is dispatched on, when
|
|
34
|
+
# `command` is not already a dotted FQN
|
|
35
|
+
# @param kind [Symbol] `:command` or `:query`
|
|
36
|
+
# @return [void]
|
|
29
37
|
def tests(command, on: nil, kind: :command)
|
|
30
38
|
@tests_command = command
|
|
31
39
|
@on_aggregate = on
|
|
32
40
|
@kind = kind
|
|
33
41
|
end
|
|
34
42
|
|
|
43
|
+
# Records one setup command to dispatch before the test itself runs.
|
|
44
|
+
#
|
|
45
|
+
# @param command [String, Symbol] the setup command's verb
|
|
46
|
+
# @param kwargs [Hash{Symbol => Object}] the setup command's own arguments
|
|
47
|
+
# @return [void]
|
|
35
48
|
def setup(command, **kwargs)
|
|
36
49
|
@setups << TestSetup.new(command: command, args: kwargs)
|
|
37
50
|
end
|
|
38
51
|
|
|
52
|
+
# Merges fields into the arguments the tested command or query is called with.
|
|
53
|
+
#
|
|
54
|
+
# @param kwargs [Hash{Symbol => Object}] argument names and values to merge in
|
|
55
|
+
# @return [Hash{Symbol => Object}] the input Hash so far
|
|
39
56
|
def input(**kwargs) = @input.merge!(kwargs)
|
|
57
|
+
|
|
58
|
+
# Merges fields into what this test expects of the tested dispatch.
|
|
59
|
+
#
|
|
60
|
+
# @param kwargs [Hash{Symbol => Object}] expectation keys (`ok:`, `refused:`,
|
|
61
|
+
# `emits:`, `count:`, or a field name) and their expected values
|
|
62
|
+
# @return [Hash{Symbol => Object}] the expectation Hash so far
|
|
40
63
|
def expect(**kwargs) = @expect.merge!(kwargs)
|
|
41
64
|
|
|
65
|
+
# Builds the `TestCase` this builder collected.
|
|
66
|
+
#
|
|
67
|
+
# @return [TestCase] the built test case
|
|
68
|
+
# @raise [Malformed] if `tests` was never called, `expect` is empty, or
|
|
69
|
+
# `expect` declares `count:` on a command or `emits:` on a query
|
|
42
70
|
def build
|
|
43
71
|
unless @tests_command
|
|
44
72
|
raise Malformed, "test #{@description.inspect} never calls `tests` — " \
|
|
@@ -59,7 +87,7 @@ module Hecks
|
|
|
59
87
|
# nothing and passes whenever dispatch doesn't raise; `count:` on a
|
|
60
88
|
# command and `emits:` on a query are each read by neither runner
|
|
61
89
|
# (Expectations#run_command/#run_query), so they're accepted here
|
|
62
|
-
# and then silently ignored at run time. Checking both at
|
|
90
|
+
# and then silently ignored at run time. Checking both at build
|
|
63
91
|
# time, not in the runners, makes them errors on the file that
|
|
64
92
|
# wrote them rather than green checks nobody questions.
|
|
65
93
|
def validate_expect!
|
|
@@ -84,6 +112,8 @@ module Hecks
|
|
|
84
112
|
# `vision`/`loads`/`test` calls and builds a `BehaviorsSuite` (ir.rb),
|
|
85
113
|
# refusing to build one missing either `vision` or `loads` (`#build`).
|
|
86
114
|
class BehaviorsBuilder
|
|
115
|
+
# @param name [String] the suite's declared name
|
|
116
|
+
# @param source_path [String] the `.behaviors` file's own path
|
|
87
117
|
def initialize(name, source_path:)
|
|
88
118
|
@name = name
|
|
89
119
|
@source_path = source_path
|
|
@@ -93,21 +123,40 @@ module Hecks
|
|
|
93
123
|
@tests = []
|
|
94
124
|
end
|
|
95
125
|
|
|
126
|
+
# Records the suite's one-line description of what it is examples of.
|
|
127
|
+
#
|
|
128
|
+
# @param text [String] the vision text
|
|
129
|
+
# @return [String] `text`, unchanged
|
|
96
130
|
def vision(text) = @vision = text
|
|
97
131
|
|
|
98
|
-
# Relative to
|
|
132
|
+
# Relative to this `.behaviors` file, never to the filesystem's cwd
|
|
99
133
|
# or a same-stem convention — scope is a fact this file declares,
|
|
100
134
|
# not one a runner infers.
|
|
135
|
+
#
|
|
136
|
+
# @param paths [Array<String>] paths to the files this suite's domain boots
|
|
137
|
+
# from, relative to the `.behaviors` file
|
|
138
|
+
# @return [void]
|
|
101
139
|
def loads(*paths)
|
|
102
140
|
@loads = paths.map { |path| File.expand_path(path, @source_dir) }
|
|
103
141
|
end
|
|
104
142
|
|
|
143
|
+
# Builds and records one test case.
|
|
144
|
+
#
|
|
145
|
+
# @param description [String] the test's own description
|
|
146
|
+
# @yield the test's body, evaluated against a `TestCaseBuilder`
|
|
147
|
+
# @return [void]
|
|
148
|
+
# @raise [Malformed] if the test never calls `tests`, has no `expect`, or
|
|
149
|
+
# declares `count:` on a command or `emits:` on a query
|
|
105
150
|
def test(description, &block)
|
|
106
151
|
builder = TestCaseBuilder.new(description)
|
|
107
152
|
builder.instance_eval(&block) if block
|
|
108
153
|
@tests << builder.build
|
|
109
154
|
end
|
|
110
155
|
|
|
156
|
+
# Builds the `BehaviorsSuite` this builder collected.
|
|
157
|
+
#
|
|
158
|
+
# @return [BehaviorsSuite] the built suite
|
|
159
|
+
# @raise [Malformed] if the suite declares no `vision` or no `loads`
|
|
111
160
|
def build
|
|
112
161
|
unless @vision
|
|
113
162
|
raise Malformed, "#{@source_path}: no `vision \"...\"` — say in one line " \
|
|
@@ -122,6 +171,15 @@ module Hecks
|
|
|
122
171
|
tests: @tests, path: @source_path)
|
|
123
172
|
end
|
|
124
173
|
|
|
174
|
+
# Builds a suite in one call: constructs a builder, evaluates `block`
|
|
175
|
+
# against it, and builds the result.
|
|
176
|
+
#
|
|
177
|
+
# @param name [String] the suite's declared name
|
|
178
|
+
# @param source_path [String] the `.behaviors` file's own path
|
|
179
|
+
# @yield the suite's body, evaluated against the new builder
|
|
180
|
+
# @return [BehaviorsSuite] the built suite
|
|
181
|
+
# @raise [Malformed] if the suite declares no `vision` or `loads`, or any of
|
|
182
|
+
# its `test` blocks is malformed
|
|
125
183
|
def self.build(name, source_path:, &block)
|
|
126
184
|
builder = new(name, source_path: source_path)
|
|
127
185
|
builder.instance_eval(&block) if block
|
|
@@ -7,6 +7,8 @@ require_relative "../ports/persistence/binding_policy"
|
|
|
7
7
|
|
|
8
8
|
# Hecks::Behaviors::Expectations
|
|
9
9
|
#
|
|
10
|
+
# ## What it does
|
|
11
|
+
#
|
|
10
12
|
# One test case, start to finish: take the suite's own runtime (booted
|
|
11
13
|
# once for exactly what its `loads` names, reset to nothing between
|
|
12
14
|
# tests — `runtime_for`), replay `setup` dispatches, dispatch (or query)
|
|
@@ -14,20 +16,22 @@ require_relative "../ports/persistence/binding_policy"
|
|
|
14
16
|
# runner.rb the same way the file-count/sweep concern is split from a
|
|
15
17
|
# single test's own execution.
|
|
16
18
|
#
|
|
17
|
-
#
|
|
19
|
+
# ## Deviations from a prior port
|
|
20
|
+
#
|
|
21
|
+
# Two deliberate deviations from a prior port of this same idea (read
|
|
18
22
|
# before writing this, not reinvented):
|
|
19
23
|
#
|
|
20
24
|
# 1. A `setup` refusal and a refusal from the command under test are
|
|
21
|
-
# caught in
|
|
25
|
+
# caught in separate rescue scopes. The prior port wrapped the whole
|
|
22
26
|
# test body — setups included — in one `rescue *REFUSAL_CLASSES`, so a
|
|
23
27
|
# broken setup could spuriously satisfy `expect refused: "..."` if its
|
|
24
28
|
# own refusal happened to match the expected substring. Here, any
|
|
25
|
-
# domain refusal during `setup` is unconditionally an
|
|
29
|
+
# domain refusal during `setup` is unconditionally an error — the
|
|
26
30
|
# example never got to the situation it claims to test.
|
|
27
31
|
#
|
|
28
|
-
# 2. `emits:` is read off a `registry.event_log`
|
|
32
|
+
# 2. `emits:` is read off a `registry.event_log` diff around the tested
|
|
29
33
|
# dispatch, not off `Result#events` alone. `Result#events` only holds
|
|
30
|
-
# the events the
|
|
34
|
+
# the events the outermost dispatch announced; a policy's own cascade
|
|
31
35
|
# reenters through the same `Dispatcher#dispatch` (`PolicyInterpreter
|
|
32
36
|
# #deliver` → `door.reenter` → `dispatch`), and every dispatch's
|
|
33
37
|
# events — outer and reentrant alike — land in the one shared
|
|
@@ -46,6 +50,15 @@ module Hecks
|
|
|
46
50
|
REFUSAL_CLASSES = Hecks::Runtime::DOMAIN_REFUSALS
|
|
47
51
|
SPECIAL_KEYS = %i[ok refused emits count].freeze
|
|
48
52
|
|
|
53
|
+
# Runs one test case: replays its `setup` dispatches, dispatches (or queries)
|
|
54
|
+
# the command under test, and checks its `expect`.
|
|
55
|
+
#
|
|
56
|
+
# @param test [Behaviors::TestCase] the test case to run
|
|
57
|
+
# @param suite [Behaviors::BehaviorsSuite] the suite `test` belongs to
|
|
58
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher, nil] an
|
|
59
|
+
# already-booted runtime to reuse and reset; nil boots (or reuses the cached
|
|
60
|
+
# boot of) `suite.loads` via `runtime_for`
|
|
61
|
+
# @return [Result] the test's pass, fail, or error outcome
|
|
49
62
|
def run_one(test, suite, runtime: nil)
|
|
50
63
|
runtime ||= runtime_for(suite)
|
|
51
64
|
runtime.registry.reset_runtime_state!
|
|
@@ -66,18 +79,18 @@ module Hecks
|
|
|
66
79
|
error_result(test, "#{e.class}: #{e.message}")
|
|
67
80
|
end
|
|
68
81
|
|
|
69
|
-
#
|
|
82
|
+
# One boot per suite, not per test. The isolation a test needs is a
|
|
70
83
|
# runtime with nothing in it — and a boot of the same files gives
|
|
71
84
|
# exactly that back for the price of `Registry#reset_runtime_state!`
|
|
72
85
|
# instead of ~2s of loading, verifying and era-checking the same
|
|
73
86
|
# bluebook again (chess: 76 behaviours, 155s of which was booting
|
|
74
|
-
# `chess.bluebook` 76 times). Keyed by the suite's own `loads`
|
|
87
|
+
# `chess.bluebook` 76 times). Keyed by the suite's own `loads` and
|
|
75
88
|
# their mtimes, so an edited bluebook boots fresh on the next test
|
|
76
89
|
# rather than running against a stale one — the property a watch
|
|
77
90
|
# loop or a long rspec session actually relies on. `runtime:` lets
|
|
78
91
|
# a caller that already holds a booted runtime (a spec, a REPL)
|
|
79
92
|
# hand it in; it is reset the same way.
|
|
80
|
-
#
|
|
93
|
+
# Not frozen — a real cache, mutated below (`RUNTIMES[key] ||=
|
|
81
94
|
# boot_and_guard(files)`) and by #reset!. False positive for
|
|
82
95
|
# Style/MutableConstant.
|
|
83
96
|
# rubocop:disable-next Style/MutableConstant
|
|
@@ -85,6 +98,14 @@ module Hecks
|
|
|
85
98
|
RUNTIMES_LOCK = Mutex.new
|
|
86
99
|
private_constant :RUNTIMES, :RUNTIMES_LOCK
|
|
87
100
|
|
|
101
|
+
# Boots (or reuses the cached boot of) the runtime a suite's `loads` names.
|
|
102
|
+
#
|
|
103
|
+
# @param suite [Behaviors::BehaviorsSuite] the suite whose `loads` files boot
|
|
104
|
+
# the runtime
|
|
105
|
+
# @return [Runtime::Dispatcher, Runtime::RemoteDispatcher] the cached or
|
|
106
|
+
# freshly booted, Memory-only-guarded runtime
|
|
107
|
+
# @raise [Malformed] if any aggregate the suite boots is not bound to the
|
|
108
|
+
# default (Memory) adapter
|
|
88
109
|
def runtime_for(suite)
|
|
89
110
|
files = Array(suite.loads).map { |path| File.expand_path(path) }
|
|
90
111
|
key = files.map { |file| [file, File.exist?(file) ? File.mtime(file).to_f : nil] }
|
|
@@ -94,22 +115,35 @@ module Hecks
|
|
|
94
115
|
end
|
|
95
116
|
end
|
|
96
117
|
|
|
97
|
-
# `reset_runtime_state!` only drops repository
|
|
118
|
+
# `reset_runtime_state!` only drops repository objects between
|
|
98
119
|
# tests (registry.rb) — sufficient isolation for `Memory`, whose
|
|
99
120
|
# `@records` is a plain per-instance ivar, so a fresh object really
|
|
100
121
|
# is a fresh store. Against anything else (Sqlite, Postgres) the
|
|
101
122
|
# rows themselves stay put: tests leak into each other, and a
|
|
102
|
-
# suite booted against a domain's
|
|
123
|
+
# suite booted against a domain's real hecksagon writes to a real
|
|
103
124
|
# database. Refusing that wiring here, at boot, is the same shape
|
|
104
125
|
# of guard `BindingPolicy` already applies to a missing bind — the
|
|
105
126
|
# project's identity is refusing bad wiring up front, not
|
|
106
127
|
# discovering it mid-suite.
|
|
128
|
+
#
|
|
129
|
+
# @param files [Array<String>] absolute paths to the bluebook/hecksagon/world
|
|
130
|
+
# files to boot
|
|
131
|
+
# @return [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted,
|
|
132
|
+
# Memory-only-guarded runtime
|
|
133
|
+
# @raise [Malformed] if any aggregate is bound to a non-Memory adapter
|
|
107
134
|
def boot_and_guard(files)
|
|
108
135
|
runtime = Hecks::Runtime::Loader.boot_files(files, install_facade: false)
|
|
109
136
|
guard_memory_only!(runtime)
|
|
110
137
|
runtime
|
|
111
138
|
end
|
|
112
139
|
|
|
140
|
+
# Refuses a runtime where any aggregate is bound to anything other than the
|
|
141
|
+
# default (Memory) adapter, so tests can never leak state or touch a real store.
|
|
142
|
+
#
|
|
143
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted
|
|
144
|
+
# runtime to check
|
|
145
|
+
# @return [void]
|
|
146
|
+
# @raise [Malformed] if any aggregate is bound to a non-Memory adapter
|
|
113
147
|
def guard_memory_only!(runtime)
|
|
114
148
|
runtime.registry.bluebooks.each_value do |bluebook|
|
|
115
149
|
bluebook.aggregates.each do |aggregate|
|
|
@@ -127,6 +161,15 @@ module Hecks
|
|
|
127
161
|
end
|
|
128
162
|
end
|
|
129
163
|
|
|
164
|
+
# Qualifies and dispatches (or queries) the command under test, catching a
|
|
165
|
+
# domain refusal as the test's own outcome rather than an error.
|
|
166
|
+
#
|
|
167
|
+
# @param test [Behaviors::TestCase] the test case to run
|
|
168
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the suite's
|
|
169
|
+
# booted runtime
|
|
170
|
+
# @param bluebooks [Array<Bluebook::Chapter>] every chapter the suite booted,
|
|
171
|
+
# searched to qualify a bare command/query name
|
|
172
|
+
# @return [Result] the test's pass, fail, or error outcome
|
|
130
173
|
def run_tested(test, runtime, bluebooks)
|
|
131
174
|
verb = qualify(test.tests_command, test.on_aggregate, bluebooks, kind: test.kind)
|
|
132
175
|
|
|
@@ -139,6 +182,13 @@ module Hecks
|
|
|
139
182
|
check_refusal(test, e)
|
|
140
183
|
end
|
|
141
184
|
|
|
185
|
+
# Dispatches the command under test and checks its `expect`.
|
|
186
|
+
#
|
|
187
|
+
# @param test [Behaviors::TestCase] the test case to run
|
|
188
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the suite's
|
|
189
|
+
# booted runtime
|
|
190
|
+
# @param verb [String] the command's dotted FQN
|
|
191
|
+
# @return [Result] the test's pass or fail outcome
|
|
142
192
|
def run_command(test, runtime, verb)
|
|
143
193
|
before = runtime.registry.event_log.length
|
|
144
194
|
result = dispatch_command(runtime, verb, test.input)
|
|
@@ -159,17 +209,26 @@ module Hecks
|
|
|
159
209
|
check_ok(test) || check_fields(test, settled_state(runtime, verb, result)) || pass_result(test)
|
|
160
210
|
end
|
|
161
211
|
|
|
162
|
-
# A field expectation reads the aggregate
|
|
212
|
+
# A field expectation reads the aggregate as it stands once the
|
|
163
213
|
# dispatch and its whole cascade have run — the same "cascades are
|
|
164
214
|
# always on" reading `emits:` already commits to. `Result#state` is
|
|
165
|
-
# the wrong source for that: it snapshots the instance the
|
|
215
|
+
# the wrong source for that: it snapshots the instance the outer
|
|
166
216
|
# dispatch saved, and a policy's own reentrant dispatch (a ply
|
|
167
217
|
# advancing off a Moved event, a move count bumping) hydrates and
|
|
168
|
-
# saves a
|
|
218
|
+
# saves a fresh record afterward — so a field the cascade wrote
|
|
169
219
|
# read back stale (found live: `expect move_count: 1` got 0 while
|
|
170
220
|
# `emits:` saw MoveCountBumped in the same test). The repository
|
|
171
221
|
# holds the settled record; read it back by the id the dispatch
|
|
172
222
|
# itself answered with.
|
|
223
|
+
#
|
|
224
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the suite's
|
|
225
|
+
# booted runtime
|
|
226
|
+
# @param verb [String] the dispatched command's dotted FQN
|
|
227
|
+
# @param result [Runtime::Dispatcher::Result, Runtime::RemoteDispatcher::Result]
|
|
228
|
+
# the dispatch's own result
|
|
229
|
+
# @return [Hash{Symbol => Object}] the settled record's current state, read
|
|
230
|
+
# back from the repository; `result.state` (or `{}`) when the result has no
|
|
231
|
+
# id or the repository no longer has that aggregate/record
|
|
173
232
|
def settled_state(runtime, verb, result)
|
|
174
233
|
return result.state || {} unless result.respond_to?(:id) && result.id
|
|
175
234
|
|
|
@@ -185,7 +244,7 @@ module Hecks
|
|
|
185
244
|
# A behaviors test writes a dispatch the way the guide's own chess
|
|
186
245
|
# examples do — receiver identity and command facts side by side
|
|
187
246
|
# (`label: "g", id: "wn", to: { file: 2, rank: 2 }`) — and since
|
|
188
|
-
# #335 the dispatcher's own `to:` keyword is the
|
|
247
|
+
# #335 the dispatcher's own `to:` keyword is the routing envelope,
|
|
189
248
|
# so forwarding those kwargs loose collides the moment a domain
|
|
190
249
|
# declares a command fact named `to` (chess does: every Move's own
|
|
191
250
|
# destination). Found live: every such test failed with "to: does
|
|
@@ -202,15 +261,26 @@ module Hecks
|
|
|
202
261
|
# expects a command's own declared attributes at the top level,
|
|
203
262
|
# not a port operation's already-wrapped `to:`/`with:` shape.
|
|
204
263
|
#
|
|
205
|
-
#
|
|
206
|
-
#
|
|
207
|
-
# asked it to resolve
|
|
208
|
-
# `trigger` a port operation
|
|
209
|
-
# own port-operation branch),
|
|
210
|
-
#
|
|
211
|
-
#
|
|
264
|
+
# Checks for a port operation directly rather than leaning on
|
|
265
|
+
# `resolve_target` to raise `UnknownVerb` for one — a refusal that held
|
|
266
|
+
# only so long as nothing else ever asked it to resolve a port-operation
|
|
267
|
+
# verb. Now that a `policy` can legitimately `trigger` a port operation
|
|
268
|
+
# (`ReactionInvocation#resolve_target`'s own port-operation branch),
|
|
269
|
+
# that raise no longer happens, so this checks directly instead.
|
|
270
|
+
#
|
|
271
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the suite's
|
|
272
|
+
# booted runtime
|
|
273
|
+
# @param verb [String] the command's dotted FQN, or a port operation's
|
|
274
|
+
# @param args [Hash{Symbol => Object}] the command's (or setup's) facts, mixing
|
|
275
|
+
# receiver identity and declared arguments
|
|
276
|
+
# @return [Runtime::Dispatcher::Result, Runtime::RemoteDispatcher::Result] the
|
|
277
|
+
# dispatch's own result
|
|
278
|
+
# @raise [Runtime::UnknownVerb] if `verb` is not fully qualified or names an
|
|
279
|
+
# undeclared domain, aggregate, command, entity or port operation
|
|
280
|
+
# @raise [StandardError] any class in `Runtime::DOMAIN_REFUSALS` when the
|
|
281
|
+
# domain refuses the call
|
|
212
282
|
def dispatch_command(runtime, verb, args)
|
|
213
|
-
return runtime.
|
|
283
|
+
return runtime.dispatch_flat(verb, args) if port_operation?(runtime, verb)
|
|
214
284
|
|
|
215
285
|
invocation = begin
|
|
216
286
|
Runtime::ReactionInvocation.build(registry: runtime.registry, verb: verb,
|
|
@@ -218,7 +288,7 @@ module Hecks
|
|
|
218
288
|
rescue Runtime::UnknownVerb
|
|
219
289
|
nil
|
|
220
290
|
end
|
|
221
|
-
return runtime.
|
|
291
|
+
return runtime.dispatch_flat(verb, args) unless invocation
|
|
222
292
|
|
|
223
293
|
if invocation.key?(:to)
|
|
224
294
|
runtime.dispatch(verb, to: invocation[:to], with: invocation[:with])
|
|
@@ -227,10 +297,15 @@ module Hecks
|
|
|
227
297
|
end
|
|
228
298
|
end
|
|
229
299
|
|
|
230
|
-
#
|
|
231
|
-
# `ReactionInvocation#resolve_target`
|
|
300
|
+
# The same "Head.Rest" shape `Dispatcher#dispatch` and
|
|
301
|
+
# `ReactionInvocation#resolve_target` both already check — a bare
|
|
232
302
|
# domain/aggregate lookup plus a port-name lookup, no command
|
|
233
303
|
# resolution needed since all this asks is whether one exists.
|
|
304
|
+
#
|
|
305
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the suite's
|
|
306
|
+
# booted runtime
|
|
307
|
+
# @param verb [String] the verb to check, dotted FQN shaped
|
|
308
|
+
# @return [Boolean] true if `verb` names a port operation on a declared aggregate
|
|
234
309
|
def port_operation?(runtime, verb)
|
|
235
310
|
domain, aggregate_name, command_path = Naming.split_verb(verb)
|
|
236
311
|
return false unless command_path
|
|
@@ -242,6 +317,13 @@ module Hecks
|
|
|
242
317
|
rest && !!aggregate.port(head)
|
|
243
318
|
end
|
|
244
319
|
|
|
320
|
+
# Runs the query under test and checks its `expect`.
|
|
321
|
+
#
|
|
322
|
+
# @param test [Behaviors::TestCase] the test case to run
|
|
323
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the suite's
|
|
324
|
+
# booted runtime
|
|
325
|
+
# @param verb [String] the query's dotted FQN
|
|
326
|
+
# @return [Result] the test's pass or fail outcome
|
|
245
327
|
def run_query(test, runtime, verb)
|
|
246
328
|
rows = runtime.query(verb, **test.input)
|
|
247
329
|
|
|
@@ -267,6 +349,11 @@ module Hecks
|
|
|
267
349
|
check_ok(test) || check_fields(test, row) || pass_result(test)
|
|
268
350
|
end
|
|
269
351
|
|
|
352
|
+
# Whether `test.expect` names a field, not just one of the special keys.
|
|
353
|
+
#
|
|
354
|
+
# @param test [Behaviors::TestCase] the test case to check
|
|
355
|
+
# @return [Boolean] true if `test.expect` has any key besides `ok`, `refused`,
|
|
356
|
+
# `emits` or `count`
|
|
270
357
|
def field_expectations?(test)
|
|
271
358
|
test.expect.keys.any? { |key| !SPECIAL_KEYS.include?(key) }
|
|
272
359
|
end
|
|
@@ -277,6 +364,11 @@ module Hecks
|
|
|
277
364
|
# ambiguous about which row it's describing. `check_fields` itself
|
|
278
365
|
# stays row-shaped (it already is, for `run_command`'s settled
|
|
279
366
|
# state); this picks which row it reads.
|
|
367
|
+
#
|
|
368
|
+
# @param test [Behaviors::TestCase] the test case, for the fail message
|
|
369
|
+
# @param rows [Array<Hash>, Hash] the query's own result
|
|
370
|
+
# @return [Hash, Result] `rows` itself when it isn't an Array; its one row when
|
|
371
|
+
# it holds exactly one; otherwise a `fail_result` naming the row count
|
|
280
372
|
def query_row(test, rows)
|
|
281
373
|
return rows unless rows.is_a?(Array)
|
|
282
374
|
|
|
@@ -288,6 +380,11 @@ module Hecks
|
|
|
288
380
|
end
|
|
289
381
|
end
|
|
290
382
|
|
|
383
|
+
# Checks an `expect ok: true` expectation.
|
|
384
|
+
#
|
|
385
|
+
# @param test [Behaviors::TestCase] the test case to check
|
|
386
|
+
# @return [Result, nil] nil if `test.expect` has no `ok` key or expects `true`;
|
|
387
|
+
# a `fail_result` if it names anything else
|
|
291
388
|
def check_ok(test)
|
|
292
389
|
return unless test.expect.key?(:ok)
|
|
293
390
|
|
|
@@ -297,6 +394,13 @@ module Hecks
|
|
|
297
394
|
fail_result(test, "expect ok: only accepts true — got #{expected.inspect}")
|
|
298
395
|
end
|
|
299
396
|
|
|
397
|
+
# Checks every field-name expectation in `test.expect` against `state`.
|
|
398
|
+
#
|
|
399
|
+
# @param test [Behaviors::TestCase] the test case to check
|
|
400
|
+
# @param state [Hash{Symbol, String => Object}] the settled record's (or query
|
|
401
|
+
# row's) fields
|
|
402
|
+
# @return [Result, nil] nil if every expected field matches; a `fail_result` for
|
|
403
|
+
# the first field that names none of `state`'s keys or doesn't match
|
|
300
404
|
def check_fields(test, state)
|
|
301
405
|
test.expect.each do |key, expected|
|
|
302
406
|
next if SPECIAL_KEYS.include?(key)
|
|
@@ -314,6 +418,12 @@ module Hecks
|
|
|
314
418
|
nil
|
|
315
419
|
end
|
|
316
420
|
|
|
421
|
+
# Checks a caught domain refusal against `test.expect[:refused]`.
|
|
422
|
+
#
|
|
423
|
+
# @param test [Behaviors::TestCase] the test case to check
|
|
424
|
+
# @param error [StandardError] the caught refusal, a member of `REFUSAL_CLASSES`
|
|
425
|
+
# @return [Result] a `pass_result` if `test` expected this refusal's message, an
|
|
426
|
+
# `error_result` if it expected none, or a `fail_result` if the message doesn't match
|
|
317
427
|
def check_refusal(test, error)
|
|
318
428
|
expected = test.expect[:refused]
|
|
319
429
|
return error_result(test, "unexpected refusal (#{error.class}): #{error.message}") unless expected
|
|
@@ -331,8 +441,13 @@ module Hecks
|
|
|
331
441
|
# `expect` values both ways — bare (`expect kind: "bishop"`) and
|
|
332
442
|
# wrapped (`expect kind: { value: "bishop" }`). A live record's
|
|
333
443
|
# field always comes back as a `Hecks::Runtime::Value`;
|
|
334
|
-
# normalizing
|
|
444
|
+
# normalizing both sides to the same bare-scalar-or-plain-hash
|
|
335
445
|
# shape is the one comparison that accepts either spelling.
|
|
446
|
+
#
|
|
447
|
+
# @param value [Object] a stored field's value, or an `expect` value to compare
|
|
448
|
+
# it against
|
|
449
|
+
# @return [Object] the bare underlying value: unwrapped from a `Runtime::Value`,
|
|
450
|
+
# or from a `{value: ...}` Hash; unchanged otherwise
|
|
336
451
|
def normalize(value)
|
|
337
452
|
return Hecks::Runtime::Value.materialize_unwrapped(value) if value.is_a?(Hecks::Runtime::Value)
|
|
338
453
|
return normalize(value[:value]) if value.is_a?(Hash) && value.keys == [:value]
|
|
@@ -344,9 +459,18 @@ module Hecks
|
|
|
344
459
|
# can name more than one bluebook, so resolution searches every
|
|
345
460
|
# aggregate across every bluebook the suite booted for the one that
|
|
346
461
|
# actually declares the command. `on:` (when given, only ever on
|
|
347
|
-
# the
|
|
462
|
+
# the tested command — `setup` never receives it, see the DSL
|
|
348
463
|
# contract) narrows the search to one aggregate by name instead of
|
|
349
464
|
# searching all of them.
|
|
465
|
+
#
|
|
466
|
+
# @param command [String, Symbol] a bare verb, or an already-dotted FQN
|
|
467
|
+
# @param on_aggregate [String, Symbol, nil] the aggregate to search, or nil to
|
|
468
|
+
# search every aggregate of every bluebook
|
|
469
|
+
# @param bluebooks [Array<Bluebook::Chapter>] every chapter the suite booted
|
|
470
|
+
# @param kind [Symbol] `:command` or `:query`
|
|
471
|
+
# @return [String] `command` unchanged if already dotted, otherwise its resolved
|
|
472
|
+
# dotted FQN
|
|
473
|
+
# @raise [ArgumentError] if no aggregate declares `command`, or more than one does
|
|
350
474
|
def qualify(command, on_aggregate, bluebooks, kind:)
|
|
351
475
|
return command.to_s if command.to_s.include?(".")
|
|
352
476
|
|
|
@@ -354,9 +478,19 @@ module Hecks
|
|
|
354
478
|
disambiguate_qualified_name(candidates, command, kind, bluebooks)
|
|
355
479
|
end
|
|
356
480
|
|
|
357
|
-
#
|
|
481
|
+
# Finds every aggregate that could be what a bare command/query name refers to.
|
|
482
|
+
#
|
|
483
|
+
# **The search** — every (bluebook, aggregate) pair that declares a
|
|
358
484
|
# command/query named `command`, narrowed to `on_aggregate` by name
|
|
359
485
|
# when given.
|
|
486
|
+
#
|
|
487
|
+
# @param command [String, Symbol] the bare verb to search for
|
|
488
|
+
# @param on_aggregate [String, Symbol, nil] the aggregate to search, or nil to
|
|
489
|
+
# search every aggregate of every bluebook
|
|
490
|
+
# @param bluebooks [Array<Bluebook::Chapter>] every chapter the suite booted
|
|
491
|
+
# @param kind [Symbol] `:command` or `:query`
|
|
492
|
+
# @return [Array<Array(Bluebook::Chapter, Bluebook::Aggregate)>] every matching
|
|
493
|
+
# (chapter, aggregate) pair
|
|
360
494
|
def qualify_candidates(command, on_aggregate, bluebooks, kind)
|
|
361
495
|
members = kind == :query ? :queries : :commands
|
|
362
496
|
pairs =
|
|
@@ -368,9 +502,20 @@ module Hecks
|
|
|
368
502
|
pairs.select { |_, agg| agg.public_send(members).any? { |m| m.hecks_name == command.to_s } }
|
|
369
503
|
end
|
|
370
504
|
|
|
371
|
-
#
|
|
505
|
+
# Resolves a search's candidates to exactly one dotted FQN, or refuses.
|
|
506
|
+
#
|
|
507
|
+
# **The report** — zero candidates and more-than-one candidates both
|
|
372
508
|
# refuse (with a different message); exactly one resolves to its
|
|
373
509
|
# dotted FQN.
|
|
510
|
+
#
|
|
511
|
+
# @param candidates [Array<Array(Bluebook::Chapter, Bluebook::Aggregate)>] the
|
|
512
|
+
# matching (chapter, aggregate) pairs found by `qualify_candidates`
|
|
513
|
+
# @param command [String, Symbol] the bare verb that was searched for
|
|
514
|
+
# @param kind [Symbol] `:command` or `:query`, for the refusal message
|
|
515
|
+
# @param bluebooks [Array<Bluebook::Chapter>] every chapter the suite booted,
|
|
516
|
+
# for the refusal message
|
|
517
|
+
# @return [String] the one candidate's dotted FQN
|
|
518
|
+
# @raise [ArgumentError] if `candidates` is empty, or holds more than one
|
|
374
519
|
def disambiguate_qualified_name(candidates, command, kind, bluebooks)
|
|
375
520
|
case candidates.size
|
|
376
521
|
when 0
|
|
@@ -386,8 +531,24 @@ module Hecks
|
|
|
386
531
|
end
|
|
387
532
|
end
|
|
388
533
|
|
|
534
|
+
# Builds a passing result.
|
|
535
|
+
#
|
|
536
|
+
# @param test [Behaviors::TestCase] the test that passed
|
|
537
|
+
# @return [Result] a `:pass` result
|
|
389
538
|
def pass_result(test) = Result.new(description: test.description, status: :pass, message: nil)
|
|
539
|
+
|
|
540
|
+
# Builds a failing result.
|
|
541
|
+
#
|
|
542
|
+
# @param test [Behaviors::TestCase] the test whose expectation was not met
|
|
543
|
+
# @param message [String] what was expected versus what happened
|
|
544
|
+
# @return [Result] a `:fail` result
|
|
390
545
|
def fail_result(test, message) = Result.new(description: test.description, status: :fail, message: message)
|
|
546
|
+
|
|
547
|
+
# Builds an errored result.
|
|
548
|
+
#
|
|
549
|
+
# @param test [Behaviors::TestCase] the test that could not run to a conclusion
|
|
550
|
+
# @param message [String] what went wrong
|
|
551
|
+
# @return [Result] an `:error` result
|
|
391
552
|
def error_result(test, message) = Result.new(description: test.description, status: :error, message: message)
|
|
392
553
|
|
|
393
554
|
Result = Struct.new(:description, :status, :message, keyword_init: true)
|
data/lib/hecks/behaviors/ir.rb
CHANGED
|
@@ -2,24 +2,35 @@
|
|
|
2
2
|
#
|
|
3
3
|
# The `.behaviors` authoring surface's own IR — plain Structs, holding
|
|
4
4
|
# exactly what `Hecks.behaviors "Name" do ... end` collected. Deliberately
|
|
5
|
-
#
|
|
5
|
+
# not part of `Hecks::IR`/`emits_ir` — a behaviors suite is never
|
|
6
6
|
# collected into a domain Registry (see `Hecks.behaviors`), never
|
|
7
7
|
# dispatched through MetaValidator, and carries none of the self-hosted
|
|
8
8
|
# round-trip machinery a real bluebook construct does. It is a test
|
|
9
9
|
# artifact a runner reads on demand, not a domain a boot needs.
|
|
10
10
|
module Hecks
|
|
11
|
+
# The `.behaviors` toolkit: the DSL a `.behaviors` file is written against
|
|
12
|
+
# (`Hecks.behaviors`, dsl.rb), the plain-Struct IR it builds (this file), the runner
|
|
13
|
+
# that boots a suite's domain and checks each test (runner.rb, expectations.rb), and
|
|
14
|
+
# an rspec shim for a consuming app's own suite (rspec.rb).
|
|
11
15
|
module Behaviors
|
|
12
16
|
TestSetup = Struct.new(:command, :args, keyword_init: true)
|
|
13
17
|
|
|
14
18
|
TestCase = Struct.new(:description, :tests_command, :on_aggregate, :kind,
|
|
15
19
|
:setups, :input, :expect, keyword_init: true) do
|
|
20
|
+
# Tells whether `tests_command` is already a fully qualified verb name.
|
|
21
|
+
#
|
|
16
22
|
# An already-dotted tests_command is a literal FQN; otherwise `on:`
|
|
17
23
|
# composes with the domain name resolved once setups/the tested
|
|
18
24
|
# dispatch actually run (see Expectations — the domain isn't known
|
|
19
25
|
# until `loads` is booted, so it's a runtime concern, not a field
|
|
20
26
|
# here).
|
|
27
|
+
#
|
|
28
|
+
# @return [Boolean] true when `tests_command` contains a `.`
|
|
21
29
|
def dotted? = tests_command.to_s.include?(".")
|
|
22
30
|
|
|
31
|
+
# Tells whether this test case exercises a query rather than a command.
|
|
32
|
+
#
|
|
33
|
+
# @return [Boolean] true when `kind` is `:query`
|
|
23
34
|
def query? = kind == :query
|
|
24
35
|
end
|
|
25
36
|
|
|
@@ -4,7 +4,7 @@ require_relative "../behaviors"
|
|
|
4
4
|
# `bundle exec rspec` uses to run `.behaviors` files as ordinary examples,
|
|
5
5
|
# one `it` per test, named by the test's own description string. Same
|
|
6
6
|
# shape `spec/guides_spec.rb` uses for doctested guides: the file is
|
|
7
|
-
#
|
|
7
|
+
# parsed at collection time (cheap — `Behaviors.parse`, no test actually
|
|
8
8
|
# run yet, just enough to know the `it` names), and each test's own
|
|
9
9
|
# `Expectations.run_one` runs lazily inside its own `it`, exactly when
|
|
10
10
|
# rspec actually executes it.
|
|
@@ -21,6 +21,14 @@ module Hecks
|
|
|
21
21
|
module RSpec
|
|
22
22
|
module_function
|
|
23
23
|
|
|
24
|
+
# Parses one `.behaviors` file and registers an rspec example group for it, one
|
|
25
|
+
# `it` per test named by the test's own description.
|
|
26
|
+
#
|
|
27
|
+
# @param path [String] the `.behaviors` file's path
|
|
28
|
+
# @return [void]
|
|
29
|
+
# @raise [RuntimeError] not raised by this call itself; wraps `parsed.parse_error`'s
|
|
30
|
+
# message and is raised only when rspec later runs the generated "loads without a
|
|
31
|
+
# parse error" example, so it surfaces as that example's failure
|
|
24
32
|
def describe_file(path)
|
|
25
33
|
parsed = Behaviors.parse(path)
|
|
26
34
|
|