hecks 1.2.0 → 1.4.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 +7 -7
- data/lib/hecks/adapters/driven/d1.rb +187 -23
- data/lib/hecks/adapters/driven/folder.rb +10 -10
- data/lib/hecks/adapters/driven/google_authentication.rb +8 -8
- data/lib/hecks/adapters/driven/governance_authorization.rb +31 -13
- data/lib/hecks/adapters/driven/heki/journal.rb +60 -2
- data/lib/hecks/adapters/driven/heki/saga_store.rb +5 -5
- data/lib/hecks/adapters/driven/heki.rb +13 -7
- data/lib/hecks/adapters/driven/identity_registry.rb +2 -2
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +3 -3
- data/lib/hecks/adapters/driven/lambda/client.rb +34 -9
- data/lib/hecks/adapters/driven/lambda.rb +39 -33
- data/lib/hecks/adapters/driven/local_storage.rb +17 -10
- data/lib/hecks/adapters/driven/memory.rb +205 -9
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +1 -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/postgres_era.adapter +5 -0
- data/lib/hecks/adapters/driven/prism.rb +4 -4
- 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.rb +4 -4
- data/lib/hecks/adapters/driving/github_webhook.rb +145 -0
- data/lib/hecks/behaviors/dsl.rb +2 -2
- data/lib/hecks/behaviors/expectations.rb +51 -23
- data/lib/hecks/behaviors/ir.rb +1 -1
- data/lib/hecks/behaviors/rspec.rb +1 -1
- data/lib/hecks/behaviors/runner.rb +2 -2
- data/lib/hecks/behaviors.rb +1 -1
- data/lib/hecks/bluebook/aggregate.rb +13 -13
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +10 -10
- data/lib/hecks/bluebook/assembly/build.rb +1 -1
- data/lib/hecks/bluebook/assembly/contract.rb +39 -16
- data/lib/hecks/bluebook/assembly/contracts.rb +40 -38
- data/lib/hecks/bluebook/assembly/marks.rb +18 -18
- data/lib/hecks/bluebook/assembly/specializer.rb +19 -21
- data/lib/hecks/bluebook/assembly.rb +9 -9
- data/lib/hecks/bluebook/attribute.rb +9 -9
- data/lib/hecks/bluebook/behaviour/aggregate.rb +11 -11
- data/lib/hecks/bluebook/behaviour/attribute.rb +5 -5
- data/lib/hecks/bluebook/behaviour/chapter.rb +23 -5
- data/lib/hecks/bluebook/behaviour/command.rb +23 -23
- data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -3
- data/lib/hecks/bluebook/behaviour/entity.rb +8 -8
- data/lib/hecks/bluebook/behaviour/hexagon.rb +4 -4
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +5 -5
- data/lib/hecks/bluebook/behaviour/policy.rb +12 -12
- data/lib/hecks/bluebook/behaviour/process_manager.rb +7 -7
- data/lib/hecks/bluebook/behaviour/query.rb +1 -1
- data/lib/hecks/bluebook/behaviour/read_model.rb +8 -8
- data/lib/hecks/bluebook/behaviour/traits.rb +12 -12
- data/lib/hecks/bluebook/behaviour/value_object.rb +6 -6
- data/lib/hecks/bluebook/capabilities.rb +27 -0
- data/lib/hecks/bluebook/chapter.rb +28 -9
- data/lib/hecks/bluebook/command.rb +12 -12
- data/lib/hecks/bluebook/domain_port.rb +9 -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 +96 -96
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +41 -41
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +22 -2
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +111 -74
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +48 -30
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +103 -103
- 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 +56 -56
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +89 -30
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +17 -17
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
- data/lib/hecks/bluebook/dsl/policy_builder.rb +30 -21
- 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 +35 -35
- data/lib/hecks/bluebook/dsl/query_builder.rb +5 -5
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +34 -34
- data/lib/hecks/bluebook/dsl/rule_reference.rb +41 -39
- data/lib/hecks/bluebook/dsl/translation_builder.rb +9 -9
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +16 -16
- 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 +11 -11
- data/lib/hecks/bluebook/expression/ast_json.rb +20 -20
- data/lib/hecks/bluebook/expression/ast_reader.rb +3 -3
- data/lib/hecks/bluebook/expression/canonical_form.rb +9 -9
- data/lib/hecks/bluebook/expression/evaluator.rb +18 -18
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +18 -18
- data/lib/hecks/bluebook/expression/resolver.rb +60 -62
- data/lib/hecks/bluebook/hexagon.rb +1 -1
- data/lib/hecks/bluebook/lifecycle.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/judge.rb +122 -86
- data/lib/hecks/bluebook/meta_validator/plan.rb +39 -39
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -2
- data/lib/hecks/bluebook/meta_validator/readings.rb +43 -43
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +44 -37
- data/lib/hecks/bluebook/meta_validator/shapes.rb +25 -21
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +145 -31
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +6 -6
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +5 -5
- data/lib/hecks/bluebook/meta_validator.rb +70 -70
- data/lib/hecks/bluebook/model_check.rb +301 -84
- data/lib/hecks/bluebook/pattern_subset.rb +9 -9
- data/lib/hecks/bluebook/policy.rb +15 -13
- data/lib/hecks/bluebook/process_manager.rb +14 -14
- data/lib/hecks/bluebook/project_register.rb +6 -6
- data/lib/hecks/bluebook/query.rb +4 -4
- data/lib/hecks/bluebook/read_model.rb +14 -14
- data/lib/hecks/bluebook/reference.rb +8 -8
- data/lib/hecks/bluebook/smoke_test.rb +19 -19
- data/lib/hecks/bluebook/synthesizer.rb +12 -12
- data/lib/hecks/bluebook/translation.rb +4 -4
- data/lib/hecks/bluebook/value_object.rb +6 -6
- data/lib/hecks/bluebook.rb +2 -2
- data/lib/hecks/codemod/legacy_dispatch_args.rb +299 -0
- data/lib/hecks/codemod/legacy_dispatch_recorder.rb +186 -0
- data/lib/hecks/codemod.rb +36 -35
- data/lib/hecks/construct.rb +6 -6
- data/lib/hecks/corpus.rb +317 -0
- data/lib/hecks/deprecation.rb +95 -0
- data/lib/hecks/doc/reference.rb +19 -19
- data/lib/hecks/embryonaut_bluebook.rb +11 -11
- 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 +79 -32
- data/lib/hecks/facade/json_door.rb +106 -25
- data/lib/hecks/facade/surface/aggregate_door.rb +42 -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 -20
- data/lib/hecks/forms/html.rb +51 -7
- data/lib/hecks/forms/index_renderer.rb +14 -2
- data/lib/hecks/forms/params.rb +120 -23
- data/lib/hecks/forms/port_argument.rb +2 -2
- data/lib/hecks/forms/query_form_renderer.rb +2 -2
- data/lib/hecks/forms/record_renderer.rb +2 -2
- data/lib/hecks/forms/record_table.rb +1 -1
- data/lib/hecks/forms/value_object_shape.rb +3 -3
- data/lib/hecks/forms.rb +24 -4
- data/lib/hecks/fqn.rb +1 -1
- data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
- data/lib/hecks/framework.rb +48 -17
- data/lib/hecks/freezer.rb +11 -11
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +90 -90
- data/lib/hecks/fuzzing/combination_miner.rb +119 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +361 -0
- data/lib/hecks/fuzzing/coverage_campaign.rb +118 -0
- data/lib/hecks/fuzzing/differential.rb +158 -0
- data/lib/hecks/fuzzing/domain_generator.rb +694 -0
- data/lib/hecks/fuzzing/era_boundary.rb +124 -0
- data/lib/hecks/fuzzing/form_census.rb +199 -0
- data/lib/hecks/fuzzing/generated_domain_check.rb +95 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +6 -6
- data/lib/hecks/fuzzing/isolated_boot.rb +226 -38
- data/lib/hecks/fuzzing/nondeterministic.rb +67 -0
- data/lib/hecks/fuzzing/persistence_parity.rb +161 -0
- data/lib/hecks/fuzzing/properties/corrections.rb +100 -0
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +321 -41
- data/lib/hecks/fuzzing/properties/guards.rb +129 -26
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +32 -32
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +31 -11
- data/lib/hecks/fuzzing/properties/outbox.rb +142 -0
- data/lib/hecks/fuzzing/properties/querying.rb +17 -17
- data/lib/hecks/fuzzing/properties.rb +84 -39
- data/lib/hecks/fuzzing/qa_settings.rb +152 -0
- data/lib/hecks/fuzzing/replay.rb +224 -114
- data/lib/hecks/fuzzing/rotation_priority.rb +94 -0
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +113 -0
- data/lib/hecks/fuzzing/self_consistency.rb +676 -0
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +526 -0
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +101 -30
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +55 -8
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +31 -11
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +123 -32
- data/lib/hecks/fuzzing/sequence_generator.rb +152 -31
- data/lib/hecks/fuzzing/shrinker.rb +197 -0
- data/lib/hecks/fuzzing/structural_skips.rb +39 -0
- data/lib/hecks/fuzzing/sweep_depth.rb +53 -0
- data/lib/hecks/fuzzing/target_capabilities.rb +180 -0
- data/lib/hecks/fuzzing/value_generator.rb +66 -14
- data/lib/hecks/fuzzing.rb +11 -0
- data/lib/hecks/grammar/evolve.rb +10 -10
- data/lib/hecks/grammar.rb +7 -7
- data/lib/hecks/ir.rb +13 -13
- 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 +365 -15
- data/lib/hecks/language/oidc.json +5 -0
- data/lib/hecks/literal.rb +9 -9
- data/lib/hecks/naming.rb +89 -21
- data/lib/hecks/ports/access_control.rb +58 -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 +53 -11
- data/lib/hecks/ports/clock.rb +42 -23
- data/lib/hecks/ports/extraction.rb +16 -0
- data/lib/hecks/ports/identity_assignment.rb +24 -2
- data/lib/hecks/ports/identity_generation.rb +17 -3
- data/lib/hecks/ports/identity_resolution.rb +18 -1
- 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 +160 -58
- 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 +28 -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 +44 -6
- 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 +57 -4
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +338 -83
- 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 +42 -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 +18 -19
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +5 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +1 -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 +29 -6
- 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 +3 -3
- data/lib/hecks/ports/query/ordering.rb +6 -6
- data/lib/hecks/ports/query.rb +35 -0
- data/lib/hecks/projections/bootstrap_table.rb +112 -0
- data/lib/hecks/projections/diagrams.rb +75 -75
- data/lib/hecks/projections/glossary/html.rb +250 -0
- data/lib/hecks/projections/glossary/markdown.rb +105 -0
- data/lib/hecks/projections/glossary/mermaid.rb +110 -0
- data/lib/hecks/projections/glossary/page.css +271 -0
- data/lib/hecks/projections/glossary/page.js +72 -0
- data/lib/hecks/projections/glossary/sections.rb +17 -0
- data/lib/hecks/projections/glossary/sentences.rb +205 -0
- data/lib/hecks/projections/glossary.rb +214 -286
- data/lib/hecks/projections/ir.rb +1 -1
- data/lib/hecks/projections/model/deviations.rb +18 -17
- data/lib/hecks/projections/model.rb +25 -21
- data/lib/hecks/projections/oidc.rb +7 -7
- data/lib/hecks/projections/parser_table.rb +5 -5
- data/lib/hecks/projections/reference.rb +3 -3
- data/lib/hecks/projections/rust_vocabulary.rb +443 -0
- data/lib/hecks/projections/shape.rb +2 -2
- data/lib/hecks/projections/statements.rb +11 -11
- data/lib/hecks/projections/vocabulary.rb +9 -9
- data/lib/hecks/projections.rb +5 -3
- data/lib/hecks/projector/cli_projector.rb +29 -29
- data/lib/hecks/projector/docs_projector.rb +13 -13
- data/lib/hecks/projector/exporter.rb +42 -21
- data/lib/hecks/projector/ir_projector.rb +1 -1
- data/lib/hecks/projector/narrate_projector.rb +15 -22
- data/lib/hecks/projector/target.rb +13 -13
- data/lib/hecks/projector.rb +15 -15
- data/lib/hecks/query_ir.rb +47 -47
- data/lib/hecks/query_specification/common/comparators.rb +19 -3
- data/lib/hecks/query_specification/common/comparison.rb +132 -24
- data/lib/hecks/query_specification/common/dsl.rb +65 -9
- data/lib/hecks/query_specification/common/null_policy.rb +57 -13
- 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 +3 -3
- data/lib/hecks/router/namespace_installer.rb +3 -3
- data/lib/hecks/router.rb +1 -1
- data/lib/hecks/runtime/aggregate_lock.rb +11 -11
- data/lib/hecks/runtime/boot_gates.rb +3 -3
- data/lib/hecks/runtime/caller.rb +8 -8
- data/lib/hecks/runtime/capability_graph.rb +2 -2
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +22 -25
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +64 -67
- data/lib/hecks/runtime/command_interpreter.rb +159 -102
- data/lib/hecks/runtime/command_rules/admissibility.rb +69 -69
- data/lib/hecks/runtime/command_rules/arithmetic.rb +137 -91
- 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 +94 -19
- data/lib/hecks/runtime/command_rules.rb +3 -0
- data/lib/hecks/runtime/dependency_planning.rb +11 -11
- data/lib/hecks/runtime/dispatcher.rb +283 -90
- data/lib/hecks/runtime/entity_element.rb +282 -46
- data/lib/hecks/runtime/entity_interpreter.rb +160 -69
- data/lib/hecks/runtime/errors.rb +19 -19
- data/lib/hecks/runtime/event.rb +6 -6
- data/lib/hecks/runtime/identity.rb +22 -22
- data/lib/hecks/runtime/instance.rb +39 -14
- data/lib/hecks/runtime/interpreting.rb +12 -12
- data/lib/hecks/runtime/invocation.rb +276 -0
- data/lib/hecks/runtime/loader.rb +14 -14
- data/lib/hecks/runtime/outbox.rb +23 -23
- data/lib/hecks/runtime/policy_interpreter.rb +54 -54
- data/lib/hecks/runtime/port_operation_interpreter.rb +22 -19
- data/lib/hecks/runtime/query_interpreter.rb +111 -56
- data/lib/hecks/runtime/reaction_invocation.rb +76 -9
- data/lib/hecks/runtime/read_model_interpreter.rb +40 -40
- data/lib/hecks/runtime/rebuild_sweep.rb +4 -4
- data/lib/hecks/runtime/reference_hop.rb +6 -6
- data/lib/hecks/runtime/refusal_wording.rb +92 -112
- data/lib/hecks/runtime/registry/saga_persistence.rb +21 -21
- data/lib/hecks/runtime/registry/verification.rb +36 -26
- data/lib/hecks/runtime/registry.rb +56 -27
- data/lib/hecks/runtime/remote_dispatcher.rb +38 -23
- data/lib/hecks/runtime/routing.rb +10 -88
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +17 -17
- data/lib/hecks/runtime/saga_interpreter.rb +93 -56
- data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
- data/lib/hecks/runtime/tenant_check.rb +9 -9
- data/lib/hecks/runtime/tenant_scope.rb +5 -5
- data/lib/hecks/runtime/value/admission.rb +75 -30
- data/lib/hecks/runtime/value/coercion.rb +379 -226
- data/lib/hecks/runtime/value/entity_list_coercion.rb +248 -0
- data/lib/hecks/runtime/value.rb +28 -23
- data/lib/hecks/runtime.rb +7 -7
- data/lib/hecks/storehouse.rb +64 -64
- data/lib/hecks/version.rb +3 -3
- data/lib/hecks/vocabulary.rb +207 -5
- data/lib/hecks.rb +13 -11
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +11 -11
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +14 -12
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +11 -11
- metadata +43 -2
data/lib/hecks/doc/reference.rb
CHANGED
|
@@ -6,7 +6,7 @@ module Hecks
|
|
|
6
6
|
# The DSL reference, projected from the language's own Syntax chapter
|
|
7
7
|
# — the same Keyword/Argument rows the conformance specs hold equal
|
|
8
8
|
# to the live builders. Nothing here is described twice: the tables
|
|
9
|
-
# come from the declaration, the
|
|
9
|
+
# come from the declaration, the prose is hand-written between
|
|
10
10
|
# markers the generator preserves, and the golden spec refuses a
|
|
11
11
|
# tree where the two have drifted.
|
|
12
12
|
#
|
|
@@ -19,9 +19,9 @@ module Hecks
|
|
|
19
19
|
GENERATED_END = "<!-- generated:end -->".freeze
|
|
20
20
|
TODO_SENTINEL = "<!-- TODO: document this word -->".freeze
|
|
21
21
|
|
|
22
|
-
# A PAGE'S
|
|
22
|
+
# A PAGE'S own hand-written opening, harvested under a key no word
|
|
23
23
|
# can ever collide with (words are strings off the Syntax chapter;
|
|
24
|
-
# this is a Symbol). It exists so a page can boot
|
|
24
|
+
# this is a Symbol). It exists so a page can boot once — load a real
|
|
25
25
|
# corpus chapter, wire its hexagon — and have every word's example
|
|
26
26
|
# below run against that single boot, the way a guide's opening
|
|
27
27
|
# `ruby boot` block already does. Without it each word would have to
|
|
@@ -33,7 +33,7 @@ module Hecks
|
|
|
33
33
|
|
|
34
34
|
def generated_begin(word) = "<!-- generated:begin word=#{word} -->"
|
|
35
35
|
|
|
36
|
-
# Keyed by
|
|
36
|
+
# Keyed by region rather than by word — the same marker convention,
|
|
37
37
|
# used for the parts of a page that are not about one word: a
|
|
38
38
|
# page's generated lede here, README's generated indexes below.
|
|
39
39
|
def region_begin(id) = "<!-- generated:begin id=#{id} -->"
|
|
@@ -56,7 +56,7 @@ module Hecks
|
|
|
56
56
|
# same shape `rows` used to produce — nothing below this needed to
|
|
57
57
|
# change.
|
|
58
58
|
#
|
|
59
|
-
#
|
|
59
|
+
# No separate `@keywords ||=` here anymore. This module used to
|
|
60
60
|
# memoize its own copy on top of `SyntaxBoot.call`'s own memo — a
|
|
61
61
|
# double cache with no way to invalidate either half, and a real
|
|
62
62
|
# bug: whichever call in the whole process happened to land first
|
|
@@ -86,9 +86,9 @@ module Hecks
|
|
|
86
86
|
end.merge("index.md" => render_index)
|
|
87
87
|
end
|
|
88
88
|
|
|
89
|
-
# A WORD
|
|
89
|
+
# A WORD admitting two forms has two rows — syntax.bluebook's own
|
|
90
90
|
# stated rule, and `identified_by` (a block, or a bare argument and
|
|
91
|
-
# none) is the case that made it real again. One
|
|
91
|
+
# none) is the case that made it real again. One section per word all
|
|
92
92
|
# the same: the prose is the word's rather than the form's, and the
|
|
93
93
|
# argument rows join by (word, context) and so already cover every
|
|
94
94
|
# form. Grouped rather than rendered per row, or a reader would meet
|
|
@@ -128,7 +128,7 @@ module Hecks
|
|
|
128
128
|
inside.empty? ? "Words available in the #{context} body." : "Words available inside #{inside}."
|
|
129
129
|
end
|
|
130
130
|
|
|
131
|
-
# One
|
|
131
|
+
# One spelling per form, everything else off the first row — the
|
|
132
132
|
# columns that differ between two forms of one word are `body` (which
|
|
133
133
|
# is what the spelling shows) and nothing else.
|
|
134
134
|
def render_word(forms, prose)
|
|
@@ -212,7 +212,7 @@ module Hecks
|
|
|
212
212
|
# preamble is invented — the older shape reads back unchanged.
|
|
213
213
|
# A single-pass line-scanning state machine (current/collecting/
|
|
214
214
|
# buffer/in_fence) — each branch mutates shared local state that
|
|
215
|
-
# carries into the
|
|
215
|
+
# carries into the next iteration, so splitting per branch would
|
|
216
216
|
# mean passing all four back and forth by reference every line.
|
|
217
217
|
# rubocop:disable-next Metrics/PerceivedComplexity
|
|
218
218
|
def harvest(text)
|
|
@@ -221,7 +221,7 @@ module Hecks
|
|
|
221
221
|
collecting = false
|
|
222
222
|
buffer = []
|
|
223
223
|
|
|
224
|
-
# A
|
|
224
|
+
# A heading inside a fence is not a heading. `## something` is an
|
|
225
225
|
# ordinary Ruby comment, and now that every word's section carries
|
|
226
226
|
# runnable code, one written at the left margin would otherwise
|
|
227
227
|
# end that section mid-example and orphan the rest of it under a
|
|
@@ -293,11 +293,11 @@ module Hecks
|
|
|
293
293
|
lines.join("\n")
|
|
294
294
|
end
|
|
295
295
|
|
|
296
|
-
# The opening comment
|
|
296
|
+
# The opening comment paragraph, not just the first line — a table
|
|
297
297
|
# cell that trails off mid-clause reads worse than one that runs a
|
|
298
298
|
# little long and says "...". A code-bearing comment (`field.name`,
|
|
299
299
|
# `pattern:`) makes naive sentence-splitting on "." or ":" cut in
|
|
300
|
-
# the wrong place, so this truncates on
|
|
300
|
+
# the wrong place, so this truncates on length alone.
|
|
301
301
|
def tool_summary(path)
|
|
302
302
|
comment_lines = []
|
|
303
303
|
started = false
|
|
@@ -317,13 +317,13 @@ module Hecks
|
|
|
317
317
|
text.length > 140 ? "#{text[0, 137]}..." : text
|
|
318
318
|
end
|
|
319
319
|
|
|
320
|
-
#
|
|
320
|
+
# One real, committed file, read fresh — not re-derived from a boot
|
|
321
321
|
# (this module never requires `hecks/projections/diagrams`, and
|
|
322
322
|
# shouldn't just to draw one example). `docs/generated/diagrams/`
|
|
323
323
|
# is already held to the declaration by `spec/diagrams_spec.rb`'s
|
|
324
324
|
# own drift check; this just quotes its own output, so the two
|
|
325
325
|
# can't independently drift from each other either — a stale
|
|
326
|
-
# Order_lifecycle.mmd fails
|
|
326
|
+
# Order_lifecycle.mmd fails that spec long before this one runs.
|
|
327
327
|
def diagram_showcase(root)
|
|
328
328
|
lifecycle = File.read(File.join(root, "docs/generated/diagrams/pizzas/Order_lifecycle.mmd")).strip
|
|
329
329
|
<<~MARKDOWN.strip
|
|
@@ -363,7 +363,7 @@ module Hecks
|
|
|
363
363
|
File.write(path, render_readme(root, File.read(path)))
|
|
364
364
|
end
|
|
365
365
|
|
|
366
|
-
# An example
|
|
366
|
+
# An example a reader can see and the harness will actually run.
|
|
367
367
|
# `ruby skip` is display-only by the doctest harness's own rule, and
|
|
368
368
|
# a hidden `<!-- doctest:boot -->` block is setup rather than an
|
|
369
369
|
# example — a word whose only "example" is invisible or inert is a
|
|
@@ -373,7 +373,7 @@ module Hecks
|
|
|
373
373
|
|
|
374
374
|
def exemplified?(prose) = prose.to_s.match?(EXAMPLE_FENCE)
|
|
375
375
|
|
|
376
|
-
#
|
|
376
|
+
# Every live WORD, paired with its prose. Both coverage gates ask a
|
|
377
377
|
# question about this same walk and differ only in what they ask of
|
|
378
378
|
# the prose, so they share it rather than each re-deriving the page
|
|
379
379
|
# set — the two are meant to move together, and one drifting past
|
|
@@ -394,18 +394,18 @@ module Hecks
|
|
|
394
394
|
|
|
395
395
|
def name_of(word, context) = "#{word} (#{context})"
|
|
396
396
|
|
|
397
|
-
# The coverage gate's question: every
|
|
397
|
+
# The coverage gate's question: every live word with no prose yet.
|
|
398
398
|
def undocumented(directory)
|
|
399
399
|
live_words(directory).reject { |_word, _context, prose| prose }
|
|
400
400
|
.map { |word, context, _| name_of(word, context) }
|
|
401
401
|
end
|
|
402
402
|
|
|
403
|
-
# The
|
|
403
|
+
# The second coverage gate: prose is a declaration, and a
|
|
404
404
|
# declaration nothing runs cannot disagree with anything. A word
|
|
405
405
|
# documented only in sentences can go stale — or describe a word
|
|
406
406
|
# the runtime never wired at all, which this repository has already
|
|
407
407
|
# shipped twice (`read_model`'s where/order_by/limit/offset, and
|
|
408
|
-
# `role`/`goal` on a command). An example that
|
|
408
|
+
# `role`/`goal` on a command). An example that runs is the only
|
|
409
409
|
# documentation that can go red.
|
|
410
410
|
def unexemplified(directory)
|
|
411
411
|
live_words(directory).reject { |_word, _context, prose| exemplified?(prose) }
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
module Hecks
|
|
2
|
-
# A
|
|
2
|
+
# **A vendored, external bluebook** — same shape as Framework (framework.rb),
|
|
3
3
|
# for members that don't ship inside hecks's own lib/ at all: a
|
|
4
4
|
# separate, independently-versioned package
|
|
5
5
|
# (github.com/chrisyoung/embryonaut_bluebooks) that a consuming project
|
|
6
|
-
# vendors into its
|
|
6
|
+
# vendors into its own checkout, the same way a project already vendors
|
|
7
7
|
# hecks itself (bin/vendor_hecks, vendor/hecks/).
|
|
8
8
|
#
|
|
9
|
-
#
|
|
9
|
+
# **Recovered, not rebuilt** — this module and its `uses_embryonaut_bluebook`
|
|
10
10
|
# DSL word (hecksagon_builder.rb) were built on a prior commit of this
|
|
11
11
|
# repo (933d1dd), vendored out to a real consumer (lifeadelics/domain,
|
|
12
12
|
# for embryonaut_bluebooks/payments), and then lost from this repo's own
|
|
@@ -17,19 +17,19 @@ module Hecks
|
|
|
17
17
|
# conventions rather than copied wholesale, since the two trees had
|
|
18
18
|
# otherwise diverged for weeks in both directions.
|
|
19
19
|
#
|
|
20
|
-
#
|
|
20
|
+
# Resolved from the consuming registry's own root, not this gem's
|
|
21
21
|
# __dir__ — Framework::ROOT can be a fixed, `__dir__`-relative constant
|
|
22
22
|
# because framework members ship inside this gem; an embryonaut bluebook
|
|
23
|
-
# ships inside the
|
|
23
|
+
# ships inside the consumer's own checkout instead, at
|
|
24
24
|
# `<registry.root>/vendor/embryonaut_bluebooks/<name>/bluebook/`. There
|
|
25
25
|
# is no fixed answer until a registry (and its root) actually exists, so
|
|
26
26
|
# this resolves lazily, per call — the same reason `uses_framework`
|
|
27
27
|
# itself only runs at hecksagon-build time, when a real registry is
|
|
28
28
|
# current.
|
|
29
29
|
#
|
|
30
|
-
#
|
|
30
|
+
# Every `.bluebook` file in the package, sorted — not just one. Unlike a
|
|
31
31
|
# framework member (one file, named by its own stem), a vendored package
|
|
32
|
-
# can span several bluebook files that reopen the
|
|
32
|
+
# can span several bluebook files that reopen the same `Hecks.bluebook`
|
|
33
33
|
# (embryonaut_bluebooks/payments/bluebook/{payment,payments,policies}
|
|
34
34
|
# .bluebook all reopen "Payments"). Load order matters — policies
|
|
35
35
|
# .bluebook names `Payment::Succeed` and needs the aggregate already
|
|
@@ -37,16 +37,16 @@ module Hecks
|
|
|
37
37
|
# payment < payments < policies, the same reason that package's own
|
|
38
38
|
# files are named to fall in that order in the first place.
|
|
39
39
|
#
|
|
40
|
-
#
|
|
41
|
-
# reason: a `.hecksagon`/`.port`/`.adapter` is a
|
|
40
|
+
# **Only the bluebook files** — same restriction Framework draws, same
|
|
41
|
+
# reason: a `.hecksagon`/`.port`/`.adapter` is a wiring decision
|
|
42
42
|
# (persistence, which processor adapter is bound) that belongs to
|
|
43
43
|
# whoever is deploying, never baked into the vendored package itself.
|
|
44
44
|
# embryonaut_bluebooks/payments ships its own mock `.hecksagon` for its
|
|
45
|
-
# own spec suite; a consumer declares its
|
|
45
|
+
# own spec suite; a consumer declares its own separate
|
|
46
46
|
# `Hecks.hecksagon "Payments" do ... end` to bind real storage/adapters
|
|
47
47
|
# — see Framework's own comment for the fuller reasoning, identical here.
|
|
48
48
|
#
|
|
49
|
-
#
|
|
49
|
+
# Idempotent the same way Framework.load! Is — checked against the
|
|
50
50
|
# bluebook this package actually declares (`Naming.pascal("payments")`
|
|
51
51
|
# => "Payments"), not a separate ledger. A vendored package's directory
|
|
52
52
|
# name and its declared `Hecks.bluebook` name are the one convention
|
|
@@ -2,7 +2,7 @@ require_relative "../runtime/errors"
|
|
|
2
2
|
|
|
3
3
|
module Hecks
|
|
4
4
|
module Facade
|
|
5
|
-
#
|
|
5
|
+
# **The CLI door** — where Facade meets a caller holding flat strings.
|
|
6
6
|
#
|
|
7
7
|
# `JsonDoor` beside this one translates for a caller holding parsed JSON:
|
|
8
8
|
# String keys, already-nested objects, real Integers. A command line has
|
|
@@ -12,7 +12,7 @@ module Hecks
|
|
|
12
12
|
# So this does the two things that turns into: rebuild the nesting, and
|
|
13
13
|
# give every leaf the type the chapter declared for it.
|
|
14
14
|
#
|
|
15
|
-
#
|
|
15
|
+
# The type comes from the projection, never from the value. A door that
|
|
16
16
|
# guessed — "99 looks like a number" — would send the Integer 99 for a
|
|
17
17
|
# version string of "99", and be wrong in a way nothing downstream could
|
|
18
18
|
# detect, because both are perfectly good arguments. `Projector::CliProjector`
|
|
@@ -21,8 +21,24 @@ module Hecks
|
|
|
21
21
|
module CliDoor
|
|
22
22
|
module_function
|
|
23
23
|
|
|
24
|
+
# Builds a verb's nested, typed argument Hash out of the `name=value` words on a
|
|
25
|
+
# command line.
|
|
26
|
+
#
|
|
24
27
|
# `["reference.value=BUG#1", "sequence.value=99"]` against a projected
|
|
25
28
|
# verb spec -> `{ reference: { value: "BUG#1" }, sequence: { value: 99 } }`
|
|
29
|
+
#
|
|
30
|
+
# @param spec [Hash{Symbol => Object}] one verb's entry from `Projector::CliProjector`;
|
|
31
|
+
# read for `:arguments` and the optional `:legacy_arguments`, each an Array of
|
|
32
|
+
# option Hashes with `:path`, `:type` and, for a list, `:list`
|
|
33
|
+
# @param pairs [Array<String>] the words after the verb, each `path=value`; a path
|
|
34
|
+
# may be the short form of a single-field value object (`reference` for
|
|
35
|
+
# `reference.value`)
|
|
36
|
+
# @return [Hash{Symbol => Object}] the arguments nested by path, each leaf cast to
|
|
37
|
+
# its declared type and each list option collected into an Array; `{}` for no pairs
|
|
38
|
+
# @raise [Runtime::NotFound] if a pair has no `=`, or names a path the verb does
|
|
39
|
+
# not take
|
|
40
|
+
# @raise [Runtime::TypeMismatch] if a value cannot be read as the declared Integer
|
|
41
|
+
# or Float
|
|
26
42
|
def arguments(spec, pairs)
|
|
27
43
|
# Legacy options are accepted but not printed in help. This lets an
|
|
28
44
|
# existing id=... aggregate invocation cross the new receiver boundary
|
|
@@ -46,6 +62,12 @@ module Hecks
|
|
|
46
62
|
end
|
|
47
63
|
end
|
|
48
64
|
|
|
65
|
+
# Cuts one command-line word at its first `=`, so a value may itself contain `=`.
|
|
66
|
+
#
|
|
67
|
+
# @param pair [String] one word, such as `"reference.value=BUG#1"`
|
|
68
|
+
# @return [Array(String, String)] the path and the value; the value is `""` for
|
|
69
|
+
# `"name="`
|
|
70
|
+
# @raise [Runtime::NotFound] if the word contains no `=`
|
|
49
71
|
def split(pair)
|
|
50
72
|
name, value = pair.split("=", 2)
|
|
51
73
|
raise Runtime::NotFound, "#{pair.inspect} is not name=value" if value.nil?
|
|
@@ -53,16 +75,32 @@ module Hecks
|
|
|
53
75
|
[name, value]
|
|
54
76
|
end
|
|
55
77
|
|
|
56
|
-
#
|
|
78
|
+
# Expands a bare value-object name into the full path of its only field.
|
|
79
|
+
#
|
|
80
|
+
# The short form, for the common case. Almost every value object in this
|
|
57
81
|
# corpus has exactly one field, so `reference=BUG#1` is unambiguous and
|
|
58
82
|
# is what anybody types. Expanded only when precisely one option starts
|
|
59
83
|
# with that prefix — two would be a guess, and a guess about which field
|
|
60
84
|
# a caller meant is worse than asking them to say.
|
|
85
|
+
#
|
|
86
|
+
# @param path [String] the path as typed, such as `"reference"`
|
|
87
|
+
# @param options [Hash{String => Hash}] the verb's options keyed by full path
|
|
88
|
+
# @return [String] the one full path beginning `"#{path}."`, or `path` unchanged when
|
|
89
|
+
# none or several do
|
|
61
90
|
def expand(path, options)
|
|
62
91
|
candidates = options.keys.select { |key| key.start_with?("#{path}.") }
|
|
63
92
|
candidates.length == 1 ? candidates.first : path
|
|
64
93
|
end
|
|
65
94
|
|
|
95
|
+
# Converts one command-line value to the type the chapter declares for its field.
|
|
96
|
+
#
|
|
97
|
+
# @param value [String] the text after the `=`
|
|
98
|
+
# @param type [String] the declared type name; `"Integer"`, `"Float"` and
|
|
99
|
+
# `"Boolean"` are converted, anything else leaves the value a String
|
|
100
|
+
# @return [Integer, Float, Boolean, String] the typed value; a Boolean is true only
|
|
101
|
+
# for `true`, `yes` or `1` in any letter case, and false for every other text
|
|
102
|
+
# @raise [Runtime::TypeMismatch] if the text does not parse as the declared Integer
|
|
103
|
+
# or Float
|
|
66
104
|
def cast(value, type)
|
|
67
105
|
case type
|
|
68
106
|
when "Integer" then Integer(value)
|
|
@@ -74,27 +112,36 @@ module Hecks
|
|
|
74
112
|
raise Runtime::TypeMismatch, "#{value.inspect} is not #{type} — the chapter declares this field as #{type}"
|
|
75
113
|
end
|
|
76
114
|
|
|
77
|
-
#
|
|
115
|
+
# Adds one element to a list argument, creating the list on first use.
|
|
116
|
+
#
|
|
117
|
+
# A list grows rather than overwrites, and getting this wrong is silent.
|
|
78
118
|
#
|
|
79
|
-
# `tags.value=framework tags.value=model-checker`
|
|
80
|
-
# twice
|
|
119
|
+
# `tags.value=framework tags.value=model-checker` sent through `bury`
|
|
120
|
+
# twice would store the second one alone — no refusal, no warning, one tag
|
|
81
121
|
# simply gone. That is the failure the interview named first: not the
|
|
82
122
|
# loud kind, the kind where a value is forgotten and the caller has no
|
|
83
123
|
# way to notice.
|
|
84
124
|
#
|
|
85
|
-
# A
|
|
125
|
+
# A list of one is still a list. `tags.value=flaky` produces
|
|
86
126
|
# `[{ value: "flaky" }]`, not `{ value: "flaky" }`, because the chapter
|
|
87
127
|
# declared a collection and a caller who sent one element did not
|
|
88
|
-
# thereby declare a different shape.
|
|
89
|
-
#
|
|
128
|
+
# thereby declare a different shape. A bare object must never reach a
|
|
129
|
+
# `list_of` attribute: everything downstream that walks
|
|
90
130
|
# it — a query's `contains`, a projection, the Postgres adapter's own
|
|
91
131
|
# array handling — is entitled to assume it can iterate.
|
|
92
132
|
#
|
|
93
|
-
#
|
|
133
|
+
# Multi-field elements are not supported here, deliberately. A flat
|
|
94
134
|
# command line has no way to say which `a.x=` goes with which `a.y=`,
|
|
95
135
|
# and inventing an index syntax would be a language nobody asked for.
|
|
96
136
|
# Every list in this corpus is a list of single-field value objects; a
|
|
97
137
|
# richer one is a job for `JsonDoor`, which has real nesting.
|
|
138
|
+
#
|
|
139
|
+
# @param hash [Hash{Symbol => Object}] the arguments built so far; mutated in place
|
|
140
|
+
# @param path [Array<String>] the option's path segments, at least two: the last
|
|
141
|
+
# names the element's field, the one before it names the list
|
|
142
|
+
# @param value [Integer, Float, Boolean, String] the cast value for the new element
|
|
143
|
+
# @return [Hash{Symbol => Object}] `hash`, with `{ leaf => value }` appended to the
|
|
144
|
+
# Array at the list's key
|
|
98
145
|
def append(hash, path, value)
|
|
99
146
|
*branches, leaf = path.map(&:to_sym)
|
|
100
147
|
holder = branches[0..-2].reduce(hash) { |node, key| node[key] ||= {} }
|
|
@@ -104,6 +151,13 @@ module Hecks
|
|
|
104
151
|
hash
|
|
105
152
|
end
|
|
106
153
|
|
|
154
|
+
# Sets one value at a nested path, creating each intermediate Hash on the way and
|
|
155
|
+
# overwriting whatever the leaf already held.
|
|
156
|
+
#
|
|
157
|
+
# @param hash [Hash{Symbol => Object}] the arguments built so far; mutated in place
|
|
158
|
+
# @param path [Array<String>] the option's path segments, outermost first
|
|
159
|
+
# @param value [Integer, Float, Boolean, String] the cast value to store at the leaf
|
|
160
|
+
# @return [Hash{Symbol => Object}] `hash`, with the value set under Symbol keys
|
|
107
161
|
def bury(hash, path, value)
|
|
108
162
|
*branches, leaf = path.map(&:to_sym)
|
|
109
163
|
target = branches.reduce(hash) { |node, key| node[key] ||= {} }
|
|
@@ -111,6 +165,11 @@ module Hecks
|
|
|
111
165
|
hash
|
|
112
166
|
end
|
|
113
167
|
|
|
168
|
+
# Words the refusal for an argument the verb does not take, listing what it does.
|
|
169
|
+
#
|
|
170
|
+
# @param path [String] the path as the caller typed it
|
|
171
|
+
# @param known [Array<String>] every path the verb accepts, in any order
|
|
172
|
+
# @return [String] a one-line message naming `path` and the sorted accepted paths
|
|
114
173
|
def unknown(path, known)
|
|
115
174
|
"no argument #{path.inspect} — this verb takes #{known.sort.join(', ')}"
|
|
116
175
|
end
|
|
@@ -7,23 +7,44 @@ require_relative "../ports/clock"
|
|
|
7
7
|
|
|
8
8
|
module Hecks
|
|
9
9
|
module Facade
|
|
10
|
-
#
|
|
10
|
+
# The runner behind a projected CLI.
|
|
11
11
|
#
|
|
12
|
-
# `Projector::CliProjector` answers what a domain's command line
|
|
12
|
+
# `Projector::CliProjector` answers what a domain's command line looks
|
|
13
13
|
# like; this is the twenty lines that parse against it and dispatch. It
|
|
14
14
|
# lives in `lib/` rather than in a `bin/` because more than one front door
|
|
15
15
|
# wants it — `bin/run` for whichever domain you are standing in, `bin/qc`
|
|
16
16
|
# pinned to the QA ledger — and a second copy of the parse-and-dispatch
|
|
17
17
|
# would be the exact duplication the projection exists to avoid.
|
|
18
18
|
#
|
|
19
|
-
#
|
|
19
|
+
# No IO. It answers `[text, status]` and never prints or exits, so a spec
|
|
20
20
|
# can call it without capturing streams or trapping SystemExit. The `bin/`
|
|
21
21
|
# scripts do the printing, the same division `Router` and `JsonDoor`
|
|
22
22
|
# already keep against HTTP.
|
|
23
23
|
module CliRunner
|
|
24
24
|
module_function
|
|
25
25
|
|
|
26
|
+
# Runs one command line against a booted domain: resolves the verb or question,
|
|
27
|
+
# parses its `name=value` arguments, dispatches or queries, and answers with the text
|
|
28
|
+
# to print.
|
|
29
|
+
#
|
|
26
30
|
# `[text, status]` — status 0 answered, 1 refused or misused.
|
|
31
|
+
#
|
|
32
|
+
# Only the first bluebook in the registry (the booted domain's own) is projected
|
|
33
|
+
# into verbs. `--help`, `-h`, `help` or an empty `argv` answers the usage text; a
|
|
34
|
+
# verb followed by `--help` answers that verb's help.
|
|
35
|
+
#
|
|
36
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted domain,
|
|
37
|
+
# as `Hecks.boot` returns it
|
|
38
|
+
# @param argv [Array<String>] the words after the program name: an optional `ask`,
|
|
39
|
+
# the verb or question name (bare or aggregate-qualified), then `path=value` pairs
|
|
40
|
+
# @param program [String] how the caller was invoked, echoed in usage and hints
|
|
41
|
+
# @return [Array(String, Integer)] the text to print and the exit status: 0 with
|
|
42
|
+
# usage, help, or pretty-printed JSON of the outcome; 1 with a message when the
|
|
43
|
+
# verb is unknown, an argument is wrong, a record is missing, or the domain refuses
|
|
44
|
+
# @raise [Runtime::WiringError] if a repository, or the clock adapter a `now`
|
|
45
|
+
# argument needs, cannot be resolved
|
|
46
|
+
# @raise [Runtime::StaleWrite] if concurrent writers beat the command through every
|
|
47
|
+
# retry
|
|
27
48
|
def call(runtime:, argv:, program: "bin/run")
|
|
28
49
|
bluebook = runtime.registry.bluebooks.values.first
|
|
29
50
|
cli = Projector.call(:cli, bluebook: bluebook, options: { program: program })
|
|
@@ -31,14 +52,14 @@ module Hecks
|
|
|
31
52
|
name = argv.first
|
|
32
53
|
return [cli[:usage], 0] if name.nil? || %w[--help -h help].include?(name)
|
|
33
54
|
|
|
34
|
-
# `ask`
|
|
55
|
+
# `ask` puts a question in its own namespace — a chapter may declare a
|
|
35
56
|
# command and a query of one name, and banking does.
|
|
36
57
|
asking = name == "ask"
|
|
37
58
|
argv = argv[1..] if asking
|
|
38
59
|
name = argv.first
|
|
39
60
|
return [cli[:usage], 1] if name.nil?
|
|
40
61
|
|
|
41
|
-
#
|
|
62
|
+
# Resolved through the alias map, so `pizzas create_pizza` and
|
|
42
63
|
# `pizzas order.create_pizza` reach the same verb — the aggregate is
|
|
43
64
|
# worth typing only when two of them declare the same word.
|
|
44
65
|
pool = asking ? cli[:questions] : cli[:verbs]
|
|
@@ -56,6 +77,26 @@ module Hecks
|
|
|
56
77
|
dispatch(runtime, spec, name, rest, program, asking)
|
|
57
78
|
end
|
|
58
79
|
|
|
80
|
+
# Parses one resolved verb's arguments, runs it as a query or a command, and turns
|
|
81
|
+
# the outcome, or the domain's refusal, into printable text.
|
|
82
|
+
#
|
|
83
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted domain
|
|
84
|
+
# @param spec [Hash{Symbol => Object}] the verb's entry from `Projector::CliProjector`;
|
|
85
|
+
# read for `:kind`, `:verb`, `:arguments`, `:receiver` and `:legacy_receiver`
|
|
86
|
+
# @param name [String] the verb as the caller typed it, echoed in the `--help` hint
|
|
87
|
+
# @param rest [Array<String>] the `path=value` words after the verb
|
|
88
|
+
# @param program [String] how the caller was invoked, echoed in the `--help` hint
|
|
89
|
+
# @param asking [Boolean] whether the verb was reached through `ask`, which only
|
|
90
|
+
# changes the wording of the hint
|
|
91
|
+
# @return [Array(String, Integer)] status 0 with pretty-printed JSON: the materialized
|
|
92
|
+
# rows for a query; `id`, `state` and event names for a command; `id` and events
|
|
93
|
+
# with payloads for a port operation. Status 1 with the message of a
|
|
94
|
+
# `Runtime::DOMAIN_REFUSALS` error, followed by a `--help` hint when it is a
|
|
95
|
+
# `Runtime::NotFound` or `Runtime::TypeMismatch`
|
|
96
|
+
# @raise [Runtime::WiringError] if a repository, or the clock adapter a `now`
|
|
97
|
+
# argument needs, cannot be resolved
|
|
98
|
+
# @raise [Runtime::StaleWrite] if concurrent writers beat the command through every
|
|
99
|
+
# retry
|
|
59
100
|
def dispatch(runtime, spec, name, rest, program, asking)
|
|
60
101
|
args = stamp_time(runtime, spec, CliDoor.arguments(spec, rest))
|
|
61
102
|
|
|
@@ -64,34 +105,36 @@ module Hecks
|
|
|
64
105
|
return [JSON.pretty_generate(rows.map { |row| JsonDoor.materialize(row) }), 0]
|
|
65
106
|
end
|
|
66
107
|
|
|
67
|
-
#
|
|
108
|
+
# The answer is scoped to what was asked. `bin/run`'s step-list form
|
|
68
109
|
# reports the whole store because a corpus run is judged on all of it;
|
|
69
110
|
# somebody who issued one verb wants that verb's outcome, and against a
|
|
70
111
|
# Postgres-backed domain the full dump is every record there has been.
|
|
71
112
|
request = CommandRequest.normalize(args, receiver: spec[:receiver],
|
|
72
113
|
legacy_receiver: spec[:legacy_receiver])
|
|
73
|
-
handle = runtime.
|
|
114
|
+
handle = runtime.dispatch_flat(spec[:verb], request)
|
|
74
115
|
return [JSON.pretty_generate(answered(handle)), 0] if handle.state.nil?
|
|
75
116
|
|
|
76
117
|
[JSON.pretty_generate(id: handle.id,
|
|
77
118
|
state: JsonDoor.materialize(handle.state),
|
|
78
119
|
events: handle.events.map(&:name)), 0]
|
|
79
120
|
rescue Runtime::NotFound, Runtime::TypeMismatch => e
|
|
80
|
-
# A
|
|
121
|
+
# A bad argument and a missing record both land here, and both want the
|
|
81
122
|
# same next step: read what the verb actually takes.
|
|
82
123
|
["#{e.message}\n\n #{program} #{'ask ' if asking}#{name} --help", 1]
|
|
83
124
|
rescue *Runtime::DOMAIN_REFUSALS => e
|
|
84
|
-
#
|
|
125
|
+
# **The refusal is the product** — the chapter's own sentence, verbatim.
|
|
85
126
|
[e.message, 1]
|
|
86
127
|
end
|
|
87
128
|
|
|
88
|
-
#
|
|
129
|
+
# Supplies the current time for a verb that takes a `now` argument the caller left out.
|
|
130
|
+
#
|
|
131
|
+
# The clock, filled in at the door.
|
|
89
132
|
#
|
|
90
133
|
# A staleness rule needs the time, and the sublanguage cannot ask for it
|
|
91
134
|
# — a `given` that read the clock would judge the same record differently
|
|
92
135
|
# on two runs, and every replay, audit and fuzz oracle here assumes it
|
|
93
|
-
# does not. So `now` stays an
|
|
94
|
-
# the question becomes who types it.
|
|
136
|
+
# does not. So `now` stays an argument the predicate merely reads, and
|
|
137
|
+
# the question becomes who types it. Left to the caller, that is:
|
|
95
138
|
#
|
|
96
139
|
# qa/quality_control target.claim id=QC held_by.value=me \
|
|
97
140
|
# now.value=$(date +%s) window.value=900
|
|
@@ -99,20 +142,20 @@ module Hecks
|
|
|
99
142
|
# which is a shell incantation in front of every claim, and one an agent
|
|
100
143
|
# gets wrong by pasting a stale number.
|
|
101
144
|
#
|
|
102
|
-
#
|
|
145
|
+
# At the door, not in the runtime, and the distinction is load-bearing.
|
|
103
146
|
# `Ports::IdentityGeneration` reasons the same question through for a
|
|
104
147
|
# minted uuid and lands on "the value is baked into the caller's args at
|
|
105
|
-
# the first live dispatch". A clock consulted
|
|
148
|
+
# the first live dispatch". A clock consulted inside the interpreter
|
|
106
149
|
# would not have that property — a recorded corpus step replayed
|
|
107
150
|
# tomorrow would quietly get tomorrow's time, and the fuzzer's oracle and
|
|
108
151
|
# the adapter-agreement gate both compare runs of exactly that shape. Here
|
|
109
152
|
# it fills only what a person or an agent is typing, and `runtime.dispatch`
|
|
110
153
|
# is left alone.
|
|
111
154
|
#
|
|
112
|
-
#
|
|
155
|
+
# An explicit value always wins, so a spec or a caller reproducing a
|
|
113
156
|
# moment says so and is believed. This only supplies what was omitted.
|
|
114
157
|
#
|
|
115
|
-
#
|
|
158
|
+
# By name, which is the one uncomfortable part. `now` is a plausible
|
|
116
159
|
# domain word and nothing declares that it means the clock. It is
|
|
117
160
|
# tolerable because this is a convenience layer rather than semantics —
|
|
118
161
|
# the verb's own help says the argument exists, dispatch is unchanged,
|
|
@@ -120,6 +163,17 @@ module Hecks
|
|
|
120
163
|
# declaration in the chapter (`attribute :now, Instant, from: :clock`),
|
|
121
164
|
# which is a language change: DSL, IR, the self-hosted grammar and its
|
|
122
165
|
# goldens. Worth doing; not worth smuggling in here.
|
|
166
|
+
#
|
|
167
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted domain,
|
|
168
|
+
# whose registry names the clock adapter
|
|
169
|
+
# @param spec [Hash{Symbol => Object}] the verb's projected entry; only an option whose
|
|
170
|
+
# `:path` starts `"now."` makes this method act
|
|
171
|
+
# @param args [Hash{Symbol => Object}] the parsed arguments; not mutated
|
|
172
|
+
# @return [Hash{Symbol => Object}] `args` itself when the verb takes no `now` or the
|
|
173
|
+
# caller gave one; otherwise a copy with `now: { value: seconds }`, the clock port's
|
|
174
|
+
# current time in whole seconds
|
|
175
|
+
# @raise [Runtime::WiringError] if the time is needed and zero or several adapters
|
|
176
|
+
# implement the clock port
|
|
123
177
|
def stamp_time(runtime, spec, args)
|
|
124
178
|
return args unless spec[:arguments].any? { |argument| argument[:path].start_with?("now.") }
|
|
125
179
|
return args if args.key?(:now)
|
|
@@ -127,12 +181,15 @@ module Hecks
|
|
|
127
181
|
args.merge(now: { value: Ports::Clock.now(runtime.registry) })
|
|
128
182
|
end
|
|
129
183
|
|
|
130
|
-
#
|
|
184
|
+
# Shapes a port operation's outcome for printing: the record asked about, and each
|
|
185
|
+
# event with its full payload.
|
|
186
|
+
#
|
|
187
|
+
# A port operation has no state, and its payload is the entire point.
|
|
131
188
|
#
|
|
132
189
|
# A command answers with the record it changed, so naming the events is
|
|
133
190
|
# enough — the interesting part is in `state`. A port operation changes
|
|
134
191
|
# no record: it asked something outside and came back with what was
|
|
135
|
-
# said, and that lives
|
|
192
|
+
# said, and that lives only in the event payload. Reporting names alone
|
|
136
193
|
# would print `SpecsCompleted` and drop the spec output on the floor.
|
|
137
194
|
#
|
|
138
195
|
# This is what makes a projected CLI usable as somebody's only door. An
|
|
@@ -140,13 +197,19 @@ module Hecks
|
|
|
140
197
|
# it asks the port and reads the answer, and if the answer is a bare
|
|
141
198
|
# event name then the door leads nowhere and it needs a shell after all.
|
|
142
199
|
#
|
|
143
|
-
#
|
|
200
|
+
# Both endings come back the same way, and the status stays 0 for both.
|
|
144
201
|
# A refusal here is not a misuse — `IssueStillOpen` and `SuiteFailed`
|
|
145
202
|
# are answers the caller asked for, correctly delivered. Exit 1 is for
|
|
146
203
|
# "you typed something wrong", and conflating the two would have a
|
|
147
204
|
# scripted agent treat a healthy no as a broken call.
|
|
205
|
+
#
|
|
206
|
+
# @param handle [Runtime::Dispatcher::Result, Runtime::RemoteDispatcher::Result] the
|
|
207
|
+
# dispatch result, which for a port operation has a nil `state`
|
|
208
|
+
# @return [Hash{Symbol => Object}] `:id`, the result's id or else the first event's
|
|
209
|
+
# (nil when there is neither), and `:events`, an Array of `{ name:, payload: }`
|
|
210
|
+
# Hashes with each payload materialized to plain data
|
|
148
211
|
def answered(handle)
|
|
149
|
-
#
|
|
212
|
+
# The ID comes off the event, because a port operation hydrates no
|
|
150
213
|
# instance and the handle's own `id` is nil by design. The event knows
|
|
151
214
|
# which record was asked about — it was stamped with it — and printing
|
|
152
215
|
# `null` beside a payload that plainly says `SW-TOOL` would read as a
|
|
@@ -157,15 +220,27 @@ module Hecks
|
|
|
157
220
|
end }
|
|
158
221
|
end
|
|
159
222
|
|
|
160
|
-
#
|
|
223
|
+
# Words the answer to a verb or question that does not exist, suggesting up to five
|
|
224
|
+
# names that start the same way.
|
|
225
|
+
#
|
|
226
|
+
# A near miss is worth more than a list. Somebody who typed
|
|
161
227
|
# `bug.discovr` wants one line, not eighty-seven of them.
|
|
162
228
|
#
|
|
163
|
-
#
|
|
164
|
-
#
|
|
229
|
+
# Ranked by shared prefix, not by substring. Substring matching finds
|
|
230
|
+
# nothing for the commonest typo of all — a dropped
|
|
165
231
|
# letter, `order.create_piza`, which is a substring of nothing. Prefix
|
|
166
232
|
# length survives an error anywhere after it, which is where errors are.
|
|
233
|
+
#
|
|
234
|
+
# @param cli [Hash{Symbol => Object}] the `Projector::CliProjector` projection; read
|
|
235
|
+
# for `cli[:names]`, the alias maps under `:command` and `:question`
|
|
236
|
+
# @param name [String] what the caller typed
|
|
237
|
+
# @param asking [Boolean] whether it was typed after `ask`, which picks the question
|
|
238
|
+
# names and the wording
|
|
239
|
+
# @param program [String] how the caller was invoked, echoed in the closing hint
|
|
240
|
+
# @return [String] a multi-line message; candidates must share a prefix of at least
|
|
241
|
+
# half of `name`'s length, and never fewer than three characters
|
|
167
242
|
def unknown(cli, name, asking, program)
|
|
168
|
-
#
|
|
243
|
+
# **Both spellings are candidates**. A caller who typed the qualified
|
|
169
244
|
# form with a typo — `order.create_piza` — shares no prefix with the
|
|
170
245
|
# short name `create_pizza`, so pooling only one of them suggests
|
|
171
246
|
# nothing for half the mistakes anybody makes.
|
|
@@ -181,6 +256,12 @@ module Hecks
|
|
|
181
256
|
lines.join("\n")
|
|
182
257
|
end
|
|
183
258
|
|
|
259
|
+
# Counts how many leading characters two names have in common.
|
|
260
|
+
#
|
|
261
|
+
# @param one [String] a candidate name
|
|
262
|
+
# @param other [String] the name to compare it with
|
|
263
|
+
# @return [Integer] the length of the common prefix, from 0 up to the shorter
|
|
264
|
+
# name's length
|
|
184
265
|
def shared_prefix(one, other)
|
|
185
266
|
length = [one.length, other.length].min
|
|
186
267
|
(0...length).find { |index| one[index] != other[index] } || length
|