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
|
@@ -2,18 +2,22 @@ require_relative "../naming"
|
|
|
2
2
|
|
|
3
3
|
module Hecks
|
|
4
4
|
module Projector
|
|
5
|
-
# A
|
|
5
|
+
# A bluebook, projected as its own command-line surface.
|
|
6
6
|
#
|
|
7
7
|
# Every verb a domain declares is a subcommand; every argument is an
|
|
8
|
-
# option whose
|
|
8
|
+
# option whose type, whose admitted values and whose required-ness are
|
|
9
9
|
# already stated in the chapter. A hand-written CLI restates all of it and
|
|
10
10
|
# then drifts — and the first thing to drift is the help text, which is the
|
|
11
11
|
# only part anybody reads.
|
|
12
12
|
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
13
|
+
# ## What is projected
|
|
14
|
+
#
|
|
15
|
+
# This answers the surface only — the verb tree, the argument spec, the
|
|
16
|
+
# usage text — and nothing executes here. One small generic runner
|
|
17
|
+
# (`bin/run`) boots a domain, asks for this, parses against it and
|
|
18
|
+
# dispatches.
|
|
19
|
+
#
|
|
20
|
+
# ## Why project rather than generate
|
|
17
21
|
#
|
|
18
22
|
# The alternative was generating an executable per domain, which is what
|
|
19
23
|
# `bin/project_rust` does for a whole runtime and would be the more
|
|
@@ -23,16 +27,17 @@ module Hecks
|
|
|
23
27
|
# reads. Projecting the surface keeps one dispatcher and a help text that
|
|
24
28
|
# cannot be stale, because it is computed at the moment it is printed.
|
|
25
29
|
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
30
|
+
# ## The typing is the point
|
|
31
|
+
#
|
|
32
|
+
# A CLI hands everything over as a String. `sequence.value=99` has to
|
|
33
|
+
# become the Integer 99 or the runtime refuses it, and the only honest
|
|
34
|
+
# place to learn that is the value object's own declared field type. A CLI
|
|
35
|
+
# that guessed — "it looks like a number" — would send 99 for a version
|
|
36
|
+
# string of "99" and be wrong in a way nobody could see.
|
|
32
37
|
module CliProjector
|
|
33
38
|
module_function
|
|
34
39
|
|
|
35
|
-
#
|
|
40
|
+
# Two namespaces, not one — `{ verbs:, questions:, usage: }`.
|
|
36
41
|
#
|
|
37
42
|
# A chapter may legally declare a command and a query of one name: the
|
|
38
43
|
# language namespaces them and `Banking::Account.Open` is both, in the
|
|
@@ -45,6 +50,17 @@ module Hecks
|
|
|
45
50
|
# shell-safe where a `?` suffix would be eaten by globbing, and it makes
|
|
46
51
|
# the collision impossible rather than detected. It also reads as what it
|
|
47
52
|
# is: everything under `ask` changes nothing.
|
|
53
|
+
#
|
|
54
|
+
# @param bluebook [Bluebook::Chapter] the booted domain to project
|
|
55
|
+
# @param options [Hash{Symbol => Object}] `:program` (String, defaults to
|
|
56
|
+
# `"bin/run"`) is echoed into the usage text; `:verb` (String) and `:ask`
|
|
57
|
+
# (Boolean) select one verb's `--help` text instead of the full usage
|
|
58
|
+
# @return [Hash{Symbol => Object}] `:verbs` and `:questions` map each projected
|
|
59
|
+
# name to its spec hash; `:names` maps `:command`/`:question` to an alias table
|
|
60
|
+
# (short and full spelling both keying the full name); `:usage` is the
|
|
61
|
+
# pre-rendered help text
|
|
62
|
+
# @raise [Bluebook::DSL::Malformed] if two verbs project to the same command-line
|
|
63
|
+
# name
|
|
48
64
|
def call(bluebook:, options: {})
|
|
49
65
|
verbs = {}
|
|
50
66
|
questions = {}
|
|
@@ -62,32 +78,32 @@ module Hecks
|
|
|
62
78
|
end
|
|
63
79
|
end
|
|
64
80
|
|
|
65
|
-
# A
|
|
81
|
+
# A port is a verb too, and leaving it off the map was a real gap
|
|
66
82
|
# rather than a tasteful omission. The runtime has always dispatched
|
|
67
83
|
# a port operation by exactly the same name as a command — the
|
|
68
84
|
# projection simply never listed one, so `run_specs` and `file`
|
|
69
85
|
# answered "no such verb" while working perfectly through Ruby.
|
|
70
86
|
#
|
|
71
87
|
# It matters most for the caller with no other door. An agent that
|
|
72
|
-
# may not shell out reaches this domain
|
|
88
|
+
# may not shell out reaches this domain only through the projected
|
|
73
89
|
# CLI, and a port it cannot see is a capability it does not have.
|
|
74
90
|
aggregate.ports.each do |port|
|
|
75
91
|
port.operations.each { |o| claim(verbs, name_for(aggregate, o), port_spec(bluebook, aggregate, port, o)) }
|
|
76
92
|
end
|
|
77
93
|
end
|
|
78
94
|
|
|
79
|
-
# A
|
|
95
|
+
# A report is a question too, and leaving it off was the same gap the
|
|
80
96
|
# ports had: `Dispatcher#query` has always answered `Domain.ReportName`,
|
|
81
97
|
# the projection simply never listed one — so the composed reads worked
|
|
82
98
|
# from Ruby and did not exist for anybody whose only door is the command
|
|
83
99
|
# line.
|
|
84
100
|
#
|
|
85
|
-
# It matters most for exactly what a report is
|
|
101
|
+
# It matters most for exactly what a report is for. Every other question
|
|
86
102
|
# here answers with rows and leaves the arithmetic to the reader; a
|
|
87
103
|
# `group_by` report is the one that counts. An agent that cannot reach
|
|
88
104
|
# it can list bugs all day and never answer "how are we doing".
|
|
89
105
|
#
|
|
90
|
-
#
|
|
106
|
+
# **One dot, not two** — a report belongs to the chapter rather than to any
|
|
91
107
|
# aggregate (that is what rootless means), so it is addressed
|
|
92
108
|
# `QualityControl.BugsByStatus` where a query is
|
|
93
109
|
# `QualityControl::Bug.Queue`. `Dispatcher#query` splits on precisely
|
|
@@ -96,7 +112,7 @@ module Hecks
|
|
|
96
112
|
claim(questions, Naming.snake(model.hecks_name), report_spec(bluebook, model))
|
|
97
113
|
end
|
|
98
114
|
|
|
99
|
-
#
|
|
115
|
+
# The short spelling, where it cannot be ambiguous. `pizzas
|
|
100
116
|
# create_pizza` rather than `pizzas order.create_pizza` — the
|
|
101
117
|
# aggregate is worth typing only when two of them declare the same
|
|
102
118
|
# verb, and in a one-aggregate domain it never is. Both spellings are
|
|
@@ -114,6 +130,10 @@ module Hecks
|
|
|
114
130
|
# aggregates declaring `Close` keep `customer.close` and `account.close`,
|
|
115
131
|
# which is the honest answer — a CLI that picked one would be choosing
|
|
116
132
|
# for the caller.
|
|
133
|
+
#
|
|
134
|
+
# @param specs [Hash{String => Hash}] the verb or question map being projected,
|
|
135
|
+
# mutated in place: each spec hash gains a `:short` key
|
|
136
|
+
# @return [void]
|
|
117
137
|
def shorten(specs)
|
|
118
138
|
tails = specs.keys.group_by { |name| name.split(".").last }
|
|
119
139
|
specs.each do |name, spec|
|
|
@@ -122,6 +142,12 @@ module Hecks
|
|
|
122
142
|
end
|
|
123
143
|
end
|
|
124
144
|
|
|
145
|
+
# Builds the alias table `CliRunner` resolves a typed name against.
|
|
146
|
+
#
|
|
147
|
+
# @param specs [Hash{String => Hash}] a verb or question map whose specs already
|
|
148
|
+
# carry `:short` (set by `shorten`)
|
|
149
|
+
# @return [Hash{String => String}] every accepted spelling — the full name and
|
|
150
|
+
# its `:short` form — mapped to the full name
|
|
125
151
|
def aliases(specs)
|
|
126
152
|
specs.each_with_object({}) do |(name, spec), map|
|
|
127
153
|
map[name] = name
|
|
@@ -129,11 +155,18 @@ module Hecks
|
|
|
129
155
|
end
|
|
130
156
|
end
|
|
131
157
|
|
|
132
|
-
# A
|
|
158
|
+
# A name is claimed once. A command and a query of one name are legal in
|
|
133
159
|
# a chapter — the language namespaces them — and ambiguous as
|
|
134
160
|
# subcommands. Refused here rather than silently resolving to whichever
|
|
135
161
|
# was walked first, which is how `Ticket.Filed` (a command) and
|
|
136
162
|
# `Ticket.Filed` (a query) sat undetected until something flattened them.
|
|
163
|
+
#
|
|
164
|
+
# @param verbs [Hash{String => Hash}] the verb or question map being built,
|
|
165
|
+
# mutated in place
|
|
166
|
+
# @param name [String] the command-line name this spec claims
|
|
167
|
+
# @param spec [Hash{Symbol => Object}] the spec hash to store under `name`
|
|
168
|
+
# @return [void]
|
|
169
|
+
# @raise [Bluebook::DSL::Malformed] if `name` is already claimed by another verb
|
|
137
170
|
def claim(verbs, name, spec)
|
|
138
171
|
if verbs.key?(name)
|
|
139
172
|
raise Bluebook::DSL::Malformed,
|
|
@@ -144,6 +177,15 @@ module Hecks
|
|
|
144
177
|
verbs[name] = spec
|
|
145
178
|
end
|
|
146
179
|
|
|
180
|
+
# Builds the dotted command-line name for one verb: `aggregate[.entity].verb`,
|
|
181
|
+
# snake-cased.
|
|
182
|
+
#
|
|
183
|
+
# @param aggregate [Bluebook::Aggregate] the verb's owning aggregate
|
|
184
|
+
# @param verb [Bluebook::Command, Bluebook::Query, Bluebook::PortOperation]
|
|
185
|
+
# the command, query or port operation being named
|
|
186
|
+
# @param entity [Bluebook::Entity, nil] the entity the verb is declared on, or `nil`
|
|
187
|
+
# for an aggregate-level verb
|
|
188
|
+
# @return [String] the dotted, snake-cased command-line name
|
|
147
189
|
def name_for(aggregate, verb, entity = nil)
|
|
148
190
|
parts = [Naming.snake(aggregate.hecks_name)]
|
|
149
191
|
parts << Naming.snake(entity.hecks_name) if entity
|
|
@@ -151,12 +193,22 @@ module Hecks
|
|
|
151
193
|
parts.join(".")
|
|
152
194
|
end
|
|
153
195
|
|
|
196
|
+
# Builds the verb's fully-qualified language name, `Bluebook::Aggregate[.Entity].Verb`,
|
|
197
|
+
# for display in help text.
|
|
198
|
+
#
|
|
199
|
+
# @param bluebook [Bluebook::Chapter] the verb's owning chapter
|
|
200
|
+
# @param aggregate [Bluebook::Aggregate] the verb's owning aggregate
|
|
201
|
+
# @param verb [Bluebook::Command, Bluebook::Query, Bluebook::PortOperation]
|
|
202
|
+
# the command, query or port operation being named
|
|
203
|
+
# @param entity [Bluebook::Entity, nil] the entity the verb is declared on, or `nil`
|
|
204
|
+
# for an aggregate-level verb
|
|
205
|
+
# @return [String] the fully-qualified name, as the language spells it
|
|
154
206
|
def fqn(bluebook, aggregate, verb, entity = nil)
|
|
155
207
|
[bluebook.name, "::", aggregate.hecks_name, ".",
|
|
156
208
|
entity ? "#{entity.hecks_name}." : "", verb.hecks_name].join
|
|
157
209
|
end
|
|
158
210
|
|
|
159
|
-
#
|
|
211
|
+
# The arguments a receiver adds, before any verb-specific one. Shared by
|
|
160
212
|
# `command_spec` and `port_spec` — a port operation always addresses an
|
|
161
213
|
# aggregate record (`port_spec` passes `receiver: :aggregate`, never
|
|
162
214
|
# `:entity` or `nil`, because a port is declared on an aggregate, never
|
|
@@ -166,6 +218,14 @@ module Hecks
|
|
|
166
218
|
#
|
|
167
219
|
# `nil` — a creating command — takes none: there is no existing record
|
|
168
220
|
# yet for `to=` to name.
|
|
221
|
+
#
|
|
222
|
+
# @param receiver [Symbol, nil] `:entity`, `:aggregate`, or `nil` for a creating
|
|
223
|
+
# command that names no existing record
|
|
224
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate the record belongs to
|
|
225
|
+
# @param entity [Bluebook::Entity, nil] the entity to name, required when
|
|
226
|
+
# `receiver` is `:entity`
|
|
227
|
+
# @return [Array<Hash{Symbol => Object}>] the `to`/`to.aggregate`+`to.entity` option
|
|
228
|
+
# specs this receiver needs, or `[]` for `nil`
|
|
169
229
|
def receiver_options(receiver, aggregate, entity)
|
|
170
230
|
case receiver
|
|
171
231
|
when :entity
|
|
@@ -185,6 +245,18 @@ module Hecks
|
|
|
185
245
|
|
|
186
246
|
# ── one verb ──────────────────────────────────────────────────────
|
|
187
247
|
|
|
248
|
+
# Projects one command into the spec `verb_help` and `CliRunner` both read.
|
|
249
|
+
#
|
|
250
|
+
# @param bluebook [Bluebook::Chapter] the command's owning chapter
|
|
251
|
+
# @param aggregate [Bluebook::Aggregate] the command's owning aggregate
|
|
252
|
+
# @param entity [Bluebook::Entity, nil] the entity the command is declared on, or
|
|
253
|
+
# `nil` for an aggregate-level command
|
|
254
|
+
# @param command [Bluebook::Command] the command being projected
|
|
255
|
+
# @return [Hash{Symbol => Object}] `:verb` (fully-qualified name), `:kind` (`:command`),
|
|
256
|
+
# `:summary`, `:role` (String, nil if the command declares none), `:role_gated`
|
|
257
|
+
# (Boolean), `:creates` (Boolean), `:receiver` (`:entity`, `:aggregate` or `nil`),
|
|
258
|
+
# `:legacy_receiver` (`:id` or `nil`), `:legacy_arguments`, `:refusals`
|
|
259
|
+
# (see `refusals`) and `:arguments` (see `options_for`)
|
|
188
260
|
def command_spec(bluebook, aggregate, entity, command)
|
|
189
261
|
holder = entity || aggregate
|
|
190
262
|
arguments = command.attributes.flat_map { |a| options_for(a, holder, aggregate) }
|
|
@@ -194,7 +266,7 @@ module Hecks
|
|
|
194
266
|
(command.creates? ? nil : :aggregate)
|
|
195
267
|
end
|
|
196
268
|
|
|
197
|
-
#
|
|
269
|
+
# The receiver is not a command argument. An aggregate command names
|
|
198
270
|
# its record through to; an entity command needs both the aggregate
|
|
199
271
|
# record and the entity element within it. Keeping those paths in the
|
|
200
272
|
# projected option list makes the human-facing request complete while
|
|
@@ -215,7 +287,7 @@ module Hecks
|
|
|
215
287
|
refusals: refusals(command, holder), arguments: arguments }
|
|
216
288
|
end
|
|
217
289
|
|
|
218
|
-
# A
|
|
290
|
+
# A port operation reads as a verb but reports as a boundary.
|
|
219
291
|
#
|
|
220
292
|
# `creates: false` because it makes no record, and `refusals: []`
|
|
221
293
|
# because it has none in the sense every other verb means: a command's
|
|
@@ -223,17 +295,27 @@ module Hecks
|
|
|
223
295
|
# outbound operation's failure is somebody else's sentence, unknowable
|
|
224
296
|
# from here.
|
|
225
297
|
#
|
|
226
|
-
#
|
|
298
|
+
# The summary names both endings, which is the one thing a caller most
|
|
227
299
|
# needs and cannot infer. `run_specs` looks like it either works or
|
|
228
300
|
# errors; what it actually does is answer `SpecsCompleted` even when the
|
|
229
301
|
# suite is red, and refuse only when rspec could not run. Somebody
|
|
230
302
|
# reading `--help` should not have to open the hecksagon to find that
|
|
231
303
|
# out.
|
|
304
|
+
#
|
|
305
|
+
# @param bluebook [Bluebook::Chapter] the port's owning chapter
|
|
306
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate the port is declared on
|
|
307
|
+
# @param port [Bluebook::DomainPort] the port the operation is declared on
|
|
308
|
+
# @param operation [Bluebook::PortOperation] the operation being projected
|
|
309
|
+
# @return [Hash{Symbol => Object}] `:verb` (the `Aggregate.port.operation` name),
|
|
310
|
+
# `:kind` (`:command`), `:creates` (`false`), `:receiver` (`:aggregate`),
|
|
311
|
+
# `:refusals` (`[]`), `:role` (String, who calls whom through the port),
|
|
312
|
+
# `:role_gated` (`false`), `:summary` (see `port_summary`) and `:arguments`
|
|
313
|
+
# (see `options_for`)
|
|
232
314
|
def port_spec(bluebook, aggregate, port, operation)
|
|
233
315
|
arguments = receiver_options(:aggregate, aggregate, nil) +
|
|
234
316
|
operation.attributes.flat_map { |a| options_for(a, aggregate, aggregate) }
|
|
235
317
|
|
|
236
|
-
#
|
|
318
|
+
# The wire name carries the port, the typed name does not.
|
|
237
319
|
#
|
|
238
320
|
# `Dispatcher#dispatch` splits a verb into head and sub and looks the
|
|
239
321
|
# head up as a port, so a port operation is addressed
|
|
@@ -244,7 +326,7 @@ module Hecks
|
|
|
244
326
|
# names it short, which is the same split `shorten` already makes.
|
|
245
327
|
{ verb: [fqn(bluebook, aggregate, operation).sub(/\.[^.]+\z/, ""), port.name, operation.hecks_name].join("."),
|
|
246
328
|
kind: :command, creates: false, receiver: :aggregate, refusals: [],
|
|
247
|
-
# `role:`
|
|
329
|
+
# `role:` here is descriptive text, not an authorization gate —
|
|
248
330
|
# who calls whom through the port, for `--help`/`verb_help`'s
|
|
249
331
|
# "issued by" line. A port operation never reaches
|
|
250
332
|
# `CommandRules::Authorization#refuse_role_mismatch` (only
|
|
@@ -260,14 +342,26 @@ module Hecks
|
|
|
260
342
|
summary: port_summary(port, operation), arguments: arguments }
|
|
261
343
|
end
|
|
262
344
|
|
|
345
|
+
# Names both endings a port operation can have: what an inbound `tells` records,
|
|
346
|
+
# or what an outbound `asks` came back with and what it said instead.
|
|
347
|
+
#
|
|
348
|
+
# @param port [Bluebook::DomainPort] the operation's owning port
|
|
349
|
+
# @param operation [Bluebook::PortOperation] the operation being summarized
|
|
350
|
+
# @return [String] one line, worded for `--help`
|
|
263
351
|
def port_summary(port, operation)
|
|
264
352
|
return "#{port.name} reports it; emits #{operation.emits.join(', ')}" unless operation.outbound?
|
|
265
353
|
|
|
266
354
|
"Ask #{port.name} — answers #{operation.answers}, refuses #{operation.refuses}"
|
|
267
355
|
end
|
|
268
356
|
|
|
269
|
-
# A
|
|
357
|
+
# A rootless report takes nothing; a rooted one takes the id of the
|
|
270
358
|
# record it is a view of, under the name the model gave that reference.
|
|
359
|
+
#
|
|
360
|
+
# @param bluebook [Bluebook::Chapter] the report's owning chapter
|
|
361
|
+
# @param model [Bluebook::ReadModel] the report being projected
|
|
362
|
+
# @return [Hash{Symbol => Object}] `:verb` (`Chapter.ReportName`), `:kind`
|
|
363
|
+
# (`:query`), `:summary` and `:arguments` (`[]`, or one required id option
|
|
364
|
+
# named after the model's `reference_name` when it declares a `reference_target`)
|
|
271
365
|
def report_spec(bluebook, model)
|
|
272
366
|
arguments =
|
|
273
367
|
if model.reference_target
|
|
@@ -281,6 +375,15 @@ module Hecks
|
|
|
281
375
|
summary: model.description, arguments: arguments }
|
|
282
376
|
end
|
|
283
377
|
|
|
378
|
+
# Projects one query into the spec `verb_help` and `CliRunner` both read.
|
|
379
|
+
#
|
|
380
|
+
# @param bluebook [Bluebook::Chapter] the query's owning chapter
|
|
381
|
+
# @param aggregate [Bluebook::Aggregate] the query's owning aggregate
|
|
382
|
+
# @param entity [Bluebook::Entity, nil] the entity the query is declared on, or
|
|
383
|
+
# `nil` for an aggregate-level query
|
|
384
|
+
# @param query [Bluebook::Query] the query being projected
|
|
385
|
+
# @return [Hash{Symbol => Object}] `:verb` (fully-qualified name), `:kind`
|
|
386
|
+
# (`:query`), `:summary` and `:arguments` (see `options_for`)
|
|
284
387
|
def query_spec(bluebook, aggregate, entity, query)
|
|
285
388
|
arguments = Array(query.to_h[:attributes]).flat_map do |declared|
|
|
286
389
|
attribute = query.attributes.find { |a| a.name.to_s == declared[:name].to_s }
|
|
@@ -293,14 +396,14 @@ module Hecks
|
|
|
293
396
|
|
|
294
397
|
# ── one argument, flattened ───────────────────────────────────────
|
|
295
398
|
|
|
296
|
-
# A
|
|
399
|
+
# A value object becomes one option per field, dotted. `commit` typed
|
|
297
400
|
# `CommitRef` is `--commit.value`, because that is the shape the runtime
|
|
298
401
|
# wants and a flat `--commit` would have to guess which field it meant.
|
|
299
402
|
# Single-field value objects — almost all of them — read fine either way,
|
|
300
403
|
# and the runner accepts the short form for exactly those.
|
|
301
|
-
#
|
|
404
|
+
# Recursive, and it has to be. A value object may hold another one —
|
|
302
405
|
# pizzas' `Pizza` holds a `Price` and a `Size` — so stopping after one
|
|
303
|
-
# level produced `pizza.price_cents=1500` and sent the
|
|
406
|
+
# level produced `pizza.price_cents=1500` and sent the string "1500"
|
|
304
407
|
# where `{ cents: 1500 }` belonged.
|
|
305
408
|
#
|
|
306
409
|
# The runtime took it. `qa/FINDINGS.md` #2 is exactly that gap —
|
|
@@ -308,6 +411,18 @@ module Hecks
|
|
|
308
411
|
# one-level CLI is not merely inconvenient, it is a machine for writing
|
|
309
412
|
# malformed records into a real store, which is what it did on its first
|
|
310
413
|
# run against the pizzas database.
|
|
414
|
+
#
|
|
415
|
+
# @param attribute [Bluebook::Attribute] the field being projected
|
|
416
|
+
# @param holder [Bluebook::Aggregate, Bluebook::Entity, Class] the construct
|
|
417
|
+
# `attribute` is declared on; a `Bluebook::ValueObject` subclass on a recursive call
|
|
418
|
+
# @param aggregate [Bluebook::Aggregate] the top-level aggregate, kept across recursion
|
|
419
|
+
# so a nested value object can still be found
|
|
420
|
+
# @param prefix [String, nil] the dotted path built so far, `nil` at the top level
|
|
421
|
+
# @param optional [Boolean, nil] whether an enclosing field already makes this one
|
|
422
|
+
# optional; `nil` defers to `attribute.optional?`
|
|
423
|
+
# @return [Array<Hash{Symbol => Object}>] one option spec per leaf field, each with
|
|
424
|
+
# `:path`, `:type`, `:required`, and optionally `:note`, `:enum`, `:pattern`,
|
|
425
|
+
# `:default`, `:list`
|
|
311
426
|
def options_for(attribute, holder, aggregate, prefix = nil, optional = nil)
|
|
312
427
|
path = [prefix, attribute.name].compact.join(".")
|
|
313
428
|
optional ||= attribute.optional?
|
|
@@ -316,13 +431,13 @@ module Hecks
|
|
|
316
431
|
value_object = value_object_for(attribute, holder, aggregate)
|
|
317
432
|
return [scalar_option(path, attribute, optional)] unless value_object
|
|
318
433
|
|
|
319
|
-
# A
|
|
434
|
+
# A list says so, all the way down to its leaves.
|
|
320
435
|
#
|
|
321
436
|
# Without this a `list_of(Tag)` projected exactly like a single Tag:
|
|
322
437
|
# one option, `tags.value`, indistinguishable from a scalar. So the
|
|
323
438
|
# help said to pass one, `CliDoor#bury` overwrote the leaf each time,
|
|
324
|
-
# and passing two tags stored the second and lost the first
|
|
325
|
-
#
|
|
439
|
+
# and passing two tags stored the second and lost the first without
|
|
440
|
+
# saying anything. A missing argument is refused loudly; a forgotten
|
|
326
441
|
# one is not, which makes it the more expensive of the two by far.
|
|
327
442
|
#
|
|
328
443
|
# The flag is carried on the leaf rather than kept beside the
|
|
@@ -342,11 +457,27 @@ module Hecks
|
|
|
342
457
|
fields.map { |option| option.merge(list: true, note: [option[:note], "repeatable"].compact.join("; ")) }
|
|
343
458
|
end
|
|
344
459
|
|
|
460
|
+
# Projects a `reference_to` attribute as the id of the record it points at.
|
|
461
|
+
#
|
|
462
|
+
# @param attribute [Bluebook::Attribute] a `reference?` attribute
|
|
463
|
+
# @return [Hash{Symbol => Object}] `:path`, `:type` (`"String"`), `:required` and
|
|
464
|
+
# `:note`
|
|
345
465
|
def reference_option(attribute)
|
|
346
466
|
{ path: attribute.name.to_s, type: "String", required: !attribute.optional?,
|
|
347
467
|
note: "id of a #{attribute.type.target_name}" }
|
|
348
468
|
end
|
|
349
469
|
|
|
470
|
+
# Projects one leaf field — a plain attribute, or one field of a flattened
|
|
471
|
+
# value object.
|
|
472
|
+
#
|
|
473
|
+
# @param path [String] the dotted option path this field is reached at
|
|
474
|
+
# @param field [Bluebook::Attribute] the field being projected
|
|
475
|
+
# @param optional [Boolean] whether this option may be omitted
|
|
476
|
+
# @param enum [Array<Object>] the closed set of values this field admits, `[]` for
|
|
477
|
+
# an open field
|
|
478
|
+
# @return [Hash{Symbol => Object}] `:path`, `:type`, `:required`, plus `:enum` when
|
|
479
|
+
# `enum` is non-empty, `:pattern` when `field` declares one, and `:default` when
|
|
480
|
+
# `field` declares a non-nil one
|
|
350
481
|
def scalar_option(path, field, optional, enum: [])
|
|
351
482
|
option = { path: path, type: field.type.to_s, required: !optional }
|
|
352
483
|
option[:enum] = enum unless enum.empty?
|
|
@@ -355,12 +486,30 @@ module Hecks
|
|
|
355
486
|
option
|
|
356
487
|
end
|
|
357
488
|
|
|
489
|
+
# The values a `one_of` value object's field is closed to, for the option's
|
|
490
|
+
# `:enum`.
|
|
491
|
+
#
|
|
492
|
+
# @param value_object [Class] a `Bluebook::ValueObject` subclass
|
|
493
|
+
# @param field [Bluebook::Attribute] the value object's field being projected
|
|
494
|
+
# @return [Array<Object>] the distinct values every declared member gives this
|
|
495
|
+
# field, or `[]` when the value object declares no closed set
|
|
358
496
|
def closed_members(value_object, field)
|
|
359
497
|
return [] unless value_object.closed_set?
|
|
360
498
|
|
|
361
499
|
value_object.members.filter_map { |member| member[field.name] }.uniq
|
|
362
500
|
end
|
|
363
501
|
|
|
502
|
+
# Finds the value object type an attribute declares, if any — the fork between
|
|
503
|
+
# `scalar_option` (one option) and recursing into `options_for` (one option per
|
|
504
|
+
# field).
|
|
505
|
+
#
|
|
506
|
+
# @param attribute [Bluebook::Attribute] the field whose type is being resolved
|
|
507
|
+
# @param holder [Bluebook::Aggregate, Bluebook::Entity, Class, nil] the construct
|
|
508
|
+
# `attribute` is declared on, searched first
|
|
509
|
+
# @param aggregate [Bluebook::Aggregate, nil] the top-level aggregate, searched when
|
|
510
|
+
# `holder` does not declare the value object itself
|
|
511
|
+
# @return [Class, nil] the matching `Bluebook::ValueObject` subclass, or `nil` if
|
|
512
|
+
# `attribute`'s type names no value object in scope
|
|
364
513
|
def value_object_for(attribute, holder, aggregate)
|
|
365
514
|
[holder, aggregate].compact.each do |scope|
|
|
366
515
|
next unless scope.respond_to?(:value_objects)
|
|
@@ -373,6 +522,13 @@ module Hecks
|
|
|
373
522
|
|
|
374
523
|
# Every way this verb can say no, in the chapter's own words — printed
|
|
375
524
|
# by `--help` before the caller spends a dispatch finding out.
|
|
525
|
+
#
|
|
526
|
+
# @param command [Bluebook::Command] the command whose refusals are being projected
|
|
527
|
+
# @param holder [Bluebook::Aggregate, Bluebook::Entity] the aggregate or entity the
|
|
528
|
+
# command's lifecycle guard, if any, is checked against
|
|
529
|
+
# @return [Array<String>] one sentence per way the command can refuse: a lifecycle
|
|
530
|
+
# state mismatch, a missing referenced record per `reference_to` attribute, and
|
|
531
|
+
# each declared `given`'s description, in that order
|
|
376
532
|
def refusals(command, holder)
|
|
377
533
|
out = []
|
|
378
534
|
lifecycle = holder.lifecycle
|
|
@@ -386,11 +542,21 @@ module Hecks
|
|
|
386
542
|
|
|
387
543
|
# ── the help ──────────────────────────────────────────────────────
|
|
388
544
|
|
|
545
|
+
# Renders the full verb/question table, or one verb's `--help` text when
|
|
546
|
+
# `options[:verb]` names one.
|
|
547
|
+
#
|
|
548
|
+
# @param bluebook [Bluebook::Chapter] the projected chapter, for its name and vision
|
|
549
|
+
# line
|
|
550
|
+
# @param verbs [Hash{String => Hash}] the projected, `shorten`ed command map
|
|
551
|
+
# @param questions [Hash{String => Hash}] the projected, `shorten`ed query map
|
|
552
|
+
# @param options [Hash{Symbol => Object}] `:program` (String), `:verb` (String, nil)
|
|
553
|
+
# and `:ask` (Boolean) — see `call`
|
|
554
|
+
# @return [String] the rendered help text
|
|
389
555
|
def usage(bluebook, verbs, questions, options)
|
|
390
556
|
program = options[:program] || "bin/run"
|
|
391
557
|
only = options[:verb]
|
|
392
558
|
|
|
393
|
-
#
|
|
559
|
+
# Which namespace, when both hold the name. `options[:ask]` says so;
|
|
394
560
|
# without it a `--help` for a question would print the command that
|
|
395
561
|
# shares its name, which banking has and which is how this was found.
|
|
396
562
|
if only
|
|
@@ -416,24 +582,40 @@ module Hecks
|
|
|
416
582
|
out.join("\n")
|
|
417
583
|
end
|
|
418
584
|
|
|
585
|
+
# Picks one verb whose full spelling differs from its short one, to show the
|
|
586
|
+
# caller both forms exist.
|
|
587
|
+
#
|
|
588
|
+
# @param verbs [Hash{String => Hash}] the projected, `shorten`ed command map
|
|
589
|
+
# @return [String] `"<short> is also <full>"`, or `""` if every verb's short form
|
|
590
|
+
# is its full name (a one-aggregate domain, where no name ever collides)
|
|
419
591
|
def example_qualified(verbs)
|
|
420
592
|
name, spec = verbs.find { |key, value| key != value[:short] } || verbs.first
|
|
421
593
|
name ? "#{spec[:short]} is also #{name}" : ""
|
|
422
594
|
end
|
|
423
595
|
|
|
424
|
-
# A
|
|
596
|
+
# A query's `description` is written as a paragraph — it argues for why
|
|
425
597
|
# the list is worth reading. A verb table wants the first sentence of
|
|
426
598
|
# that argument; `--help` still prints the whole thing.
|
|
599
|
+
#
|
|
600
|
+
# @param text [String, nil] the full description, or `nil`
|
|
601
|
+
# @return [String] the text up to and including its first `.`, or `""` for `nil`
|
|
427
602
|
def first_sentence(text)
|
|
428
603
|
text.to_s.split(/(?<=\.)\s/).first.to_s
|
|
429
604
|
end
|
|
430
605
|
|
|
431
|
-
#
|
|
606
|
+
# Four text blocks, in fixed display order — meta (name/kind/role),
|
|
432
607
|
# invocation, arguments, refusals. Each block is independent of the
|
|
433
|
-
# others' content (only the
|
|
608
|
+
# others' content (only the output order is fixed, and stays fixed
|
|
434
609
|
# below), so each is its own method returning the lines it
|
|
435
610
|
# contributes — `[]` when it contributes none — concatenated in the
|
|
436
611
|
# same order the original inline version built them in.
|
|
612
|
+
#
|
|
613
|
+
# @param program [String] how the caller was invoked, echoed in the invocation line
|
|
614
|
+
# @param name [String] the verb's short display name
|
|
615
|
+
# @param spec [Hash{Symbol => Object}] the verb or question's projected spec (see
|
|
616
|
+
# `command_spec`, `port_spec`, `report_spec` or `query_spec`)
|
|
617
|
+
# @param ask [Boolean] whether this is a question, reached through `ask`
|
|
618
|
+
# @return [String] the verb's full `--help` text
|
|
437
619
|
def verb_help(program, name, spec, ask: false)
|
|
438
620
|
out = verb_help_meta_lines(name, spec)
|
|
439
621
|
out.concat(verb_help_invocation_lines(program, name, spec, ask))
|
|
@@ -442,6 +624,12 @@ module Hecks
|
|
|
442
624
|
out.join("\n")
|
|
443
625
|
end
|
|
444
626
|
|
|
627
|
+
# Renders the name/summary heading, plus the dispatched or read verb and issuing
|
|
628
|
+
# role when the spec declares them.
|
|
629
|
+
#
|
|
630
|
+
# @param name [String] the verb's short display name
|
|
631
|
+
# @param spec [Hash{Symbol => Object}] the verb or question's projected spec
|
|
632
|
+
# @return [Array<String>] the meta block's lines
|
|
445
633
|
def verb_help_meta_lines(name, spec)
|
|
446
634
|
out = ["#{name} — #{spec[:summary]}", ""]
|
|
447
635
|
out << "dispatches #{spec[:verb]}" if spec[:kind] == :command
|
|
@@ -450,11 +638,26 @@ module Hecks
|
|
|
450
638
|
out
|
|
451
639
|
end
|
|
452
640
|
|
|
641
|
+
# Renders the one-line example invocation, with every argument's path as a
|
|
642
|
+
# placeholder.
|
|
643
|
+
#
|
|
644
|
+
# @param program [String] how the caller was invoked
|
|
645
|
+
# @param name [String] the verb's short display name
|
|
646
|
+
# @param spec [Hash{Symbol => Object}] the verb or question's projected spec, read
|
|
647
|
+
# for `:arguments`
|
|
648
|
+
# @param ask [Boolean] whether to show the `ask` form of the invocation
|
|
649
|
+
# @return [Array<String>] the invocation block's lines
|
|
453
650
|
def verb_help_invocation_lines(program, name, spec, ask)
|
|
454
651
|
invocation = ask ? "#{program} ask #{name}" : "#{program} #{name}"
|
|
455
652
|
["", " #{invocation}#{spec[:arguments].map { |a| " #{a[:path]}=…" }.join}", ""]
|
|
456
653
|
end
|
|
457
654
|
|
|
655
|
+
# Renders one line per declared argument: its type, admitted values, pattern,
|
|
656
|
+
# default and whether it is optional.
|
|
657
|
+
#
|
|
658
|
+
# @param spec [Hash{Symbol => Object}] the verb or question's projected spec, read
|
|
659
|
+
# for `:arguments`
|
|
660
|
+
# @return [Array<String>] the argument block's lines, `[]` if the verb takes none
|
|
458
661
|
def verb_help_argument_lines(spec)
|
|
459
662
|
return [] if spec[:arguments].empty?
|
|
460
663
|
|
|
@@ -472,6 +675,11 @@ module Hecks
|
|
|
472
675
|
lines << ""
|
|
473
676
|
end
|
|
474
677
|
|
|
678
|
+
# Renders the "refused when:" block, one line per way the verb can say no.
|
|
679
|
+
#
|
|
680
|
+
# @param spec [Hash{Symbol => Object}] the verb or question's projected spec, read
|
|
681
|
+
# for `:refusals`
|
|
682
|
+
# @return [Array<String>] the refusal block's lines, `[]` if the verb declares none
|
|
475
683
|
def verb_help_refusal_lines(spec)
|
|
476
684
|
return [] if Array(spec[:refusals]).empty?
|
|
477
685
|
|