hecks 1.3.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 +5 -4
- 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/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 +18 -18
- data/lib/hecks/behaviors/dsl.rb +2 -2
- data/lib/hecks/behaviors/expectations.rb +23 -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 +7 -7
- 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 +101 -87
- 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 +170 -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 +180 -19
- 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 +140 -45
- 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 +37 -18
- data/lib/hecks/fuzzing/form_census.rb +35 -20
- 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 +62 -62
- data/lib/hecks/fuzzing/nondeterministic.rb +67 -0
- data/lib/hecks/fuzzing/persistence_parity.rb +19 -21
- data/lib/hecks/fuzzing/properties/corrections.rb +9 -9
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +154 -49
- data/lib/hecks/fuzzing/properties/guards.rb +42 -42
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +32 -32
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +22 -35
- data/lib/hecks/fuzzing/properties/outbox.rb +24 -24
- data/lib/hecks/fuzzing/properties/querying.rb +17 -17
- data/lib/hecks/fuzzing/properties.rb +64 -40
- data/lib/hecks/fuzzing/qa_settings.rb +152 -0
- data/lib/hecks/fuzzing/replay.rb +114 -114
- data/lib/hecks/fuzzing/rotation_priority.rb +10 -10
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +113 -0
- data/lib/hecks/fuzzing/self_consistency.rb +141 -112
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +35 -35
- 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 +58 -31
- data/lib/hecks/fuzzing/sequence_generator.rb +111 -23
- data/lib/hecks/fuzzing/shrinker.rb +197 -0
- data/lib/hecks/fuzzing/structural_skips.rb +23 -130
- data/lib/hecks/fuzzing/sweep_depth.rb +4 -4
- data/lib/hecks/fuzzing/target_capabilities.rb +49 -18
- data/lib/hecks/fuzzing/value_generator.rb +14 -14
- data/lib/hecks/fuzzing.rb +5 -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 +348 -13
- data/lib/hecks/language/oidc.json +5 -0
- data/lib/hecks/literal.rb +9 -9
- data/lib/hecks/naming.rb +25 -25
- 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 +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 +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 +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 +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 +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 +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 +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 +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 +3 -3
- data/lib/hecks/projections/glossary/markdown.rb +3 -3
- data/lib/hecks/projections/glossary/mermaid.rb +1 -1
- data/lib/hecks/projections/glossary/sections.rb +1 -1
- data/lib/hecks/projections/glossary/sentences.rb +5 -5
- data/lib/hecks/projections/glossary.rb +15 -15
- 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 +11 -11
- 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 +109 -27
- 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 +42 -63
- 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 +35 -35
- 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 +151 -67
- data/lib/hecks/runtime/entity_interpreter.rb +108 -82
- 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 +55 -64
- data/lib/hecks/runtime/reaction_invocation.rb +20 -20
- 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 -115
- 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 +45 -28
- data/lib/hecks/runtime/remote_dispatcher.rb +38 -23
- data/lib/hecks/runtime/routing.rb +10 -153
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +17 -17
- data/lib/hecks/runtime/saga_interpreter.rb +64 -64
- 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 +318 -127
- data/lib/hecks/runtime/value/entity_list_coercion.rb +34 -34
- data/lib/hecks/runtime/value.rb +21 -21
- 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 +205 -4
- 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 +22 -2
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "prism"
|
|
3
|
+
|
|
4
|
+
module Hecks
|
|
5
|
+
module Codemod
|
|
6
|
+
# The rewriting half of `bin/codemod_legacy_dispatch_args` — reads what
|
|
7
|
+
# `LegacyDispatchRecorder` observed and rewrites each agreed call site
|
|
8
|
+
# from loose keyword facts to `to:` / `with:`:
|
|
9
|
+
#
|
|
10
|
+
# runtime.dispatch("Banking::Account.FreezeAccount", number: { value: "a" })
|
|
11
|
+
# # becomes
|
|
12
|
+
# runtime.dispatch("Banking::Account.FreezeAccount", to: "a")
|
|
13
|
+
#
|
|
14
|
+
# A key that carried receiver identity and is a declared fact of the
|
|
15
|
+
# command appears in both `to:` and `with:`, and only when its expression
|
|
16
|
+
# is safe to evaluate twice.
|
|
17
|
+
#
|
|
18
|
+
# A site is rewritten only when every recorded dispatch through it split
|
|
19
|
+
# the same way, strict `with:` accepted it (or refused it with the same
|
|
20
|
+
# class the loose call was refused with), no key it would move into `to:`
|
|
21
|
+
# also rode the event payload (`to:` would empty that field for whatever
|
|
22
|
+
# reacts downstream), and the call's own text still
|
|
23
|
+
# carries exactly the recorded keys, identity keys as literal pairs.
|
|
24
|
+
# Anything else is reported with its reason and left alone. Ruby files
|
|
25
|
+
# and the runnable blocks of Markdown guides (the ones `spec/support/
|
|
26
|
+
# doctest.rb` executes) are both rewritten.
|
|
27
|
+
module LegacyDispatchArgs
|
|
28
|
+
ROUTING = %i[to with saga_correlation].freeze
|
|
29
|
+
RUNNABLE_OPENERS = ["```ruby bluebook", "```ruby boot", "```ruby", "<!-- doctest:boot"].freeze
|
|
30
|
+
|
|
31
|
+
Plan = Struct.new(:to_keys, :with_keys, :slots, keyword_init: true)
|
|
32
|
+
Outcome = Struct.new(:path, :line, :status, :reason, keyword_init: true)
|
|
33
|
+
Skip = Struct.new(:reason)
|
|
34
|
+
|
|
35
|
+
module_function
|
|
36
|
+
|
|
37
|
+
# { [path, line] => Plan or Skip }
|
|
38
|
+
def plans(recording)
|
|
39
|
+
File.foreach(recording).map { |line| JSON.parse(line) }
|
|
40
|
+
.select { |entry| entry["site"] }
|
|
41
|
+
.group_by { |entry| entry["site"].rpartition(":").then { |path, _, line| [path, line.to_i] } }
|
|
42
|
+
.transform_values { |entries| plan_for(entries) }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def plan_for(entries)
|
|
46
|
+
refused = entries.find { |entry| entry["unrewritable"] }
|
|
47
|
+
return Skip.new(refused["unrewritable"]) if refused
|
|
48
|
+
|
|
49
|
+
mismatch = entries.find { |entry| entry["strict"] && entry["strict"] != entry["outcome"] }
|
|
50
|
+
if mismatch
|
|
51
|
+
return Skip.new("with: would refuse (#{mismatch['strict']}) where the loose call ended #{mismatch['outcome']}")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
leaked = entries.flat_map { |entry| (entry["to_keys"].to_a - entry["with_keys"].to_a) & entry["payload_keys"].to_a }.uniq
|
|
55
|
+
unless leaked.empty?
|
|
56
|
+
return Skip.new("#{leaked.join(', ')} carries receiver identity AND rides the event payload, " \
|
|
57
|
+
"which `to:` would empty")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
shapes = entries.map { |entry| [entry["keys"].sort, entry["to_keys"], entry["with_keys"].sort, entry["slots"]] }.uniq
|
|
61
|
+
return Skip.new("the site dispatched #{shapes.size} differently-shaped fact sets") unless shapes.one?
|
|
62
|
+
|
|
63
|
+
first = entries.first
|
|
64
|
+
Plan.new(to_keys: first["to_keys"], with_keys: first["with_keys"], slots: first["slots"])
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# [new_text, outcomes] for one file; `site_plans` is { line => Plan or Skip }.
|
|
68
|
+
def rewrite(path, site_plans)
|
|
69
|
+
text = File.binread(path).force_encoding(Encoding::UTF_8)
|
|
70
|
+
outcomes = []
|
|
71
|
+
edits = []
|
|
72
|
+
|
|
73
|
+
site_plans.sort.each do |line, plan|
|
|
74
|
+
result = plan.is_a?(Skip) ? plan : edit_for(text, path, line, plan)
|
|
75
|
+
if result.is_a?(Skip)
|
|
76
|
+
outcomes << Outcome.new(path: path, line: line, status: :skipped, reason: result.reason)
|
|
77
|
+
else
|
|
78
|
+
edits << result
|
|
79
|
+
outcomes << Outcome.new(path: path, line: line, status: :rewritten)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
edits.uniq.sort_by { |start, _, _| -start }.each do |start, finish, replacement|
|
|
84
|
+
text = text.byteslice(0, start) + replacement + text.byteslice(finish..)
|
|
85
|
+
end
|
|
86
|
+
[text, outcomes]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# [start byte, end byte, replacement] in file coordinates, or a Skip.
|
|
90
|
+
def edit_for(text, path, line, plan)
|
|
91
|
+
offset, code, first_line = region(text, path, line)
|
|
92
|
+
return Skip.new("line #{line} is not inside a runnable Ruby block") unless code
|
|
93
|
+
|
|
94
|
+
tree = Prism.parse(code, line: first_line)
|
|
95
|
+
return Skip.new("the enclosing Ruby does not parse") unless tree.errors.empty?
|
|
96
|
+
|
|
97
|
+
calls = []
|
|
98
|
+
collect_calls(tree.value, line, calls)
|
|
99
|
+
skips = []
|
|
100
|
+
calls.sort_by { |call| call.location.length }.each do |call|
|
|
101
|
+
keywords = call.arguments.arguments.last
|
|
102
|
+
result = replacement_for(keywords, plan)
|
|
103
|
+
if result.is_a?(Skip)
|
|
104
|
+
skips << result
|
|
105
|
+
next
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
return [offset + keywords.location.start_offset, offset + keywords.location.end_offset, result]
|
|
109
|
+
end
|
|
110
|
+
skips.first || Skip.new("no call with keyword arguments at line #{line}")
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# The whole file for Ruby; for Markdown, the runnable block containing
|
|
114
|
+
# `line`. [byte offset of the block, its code, its first line number].
|
|
115
|
+
def region(text, path, line)
|
|
116
|
+
return [0, text, 1] unless path.end_with?(".md")
|
|
117
|
+
|
|
118
|
+
offset = 0
|
|
119
|
+
open = nil
|
|
120
|
+
text.each_line.with_index(1) do |source_line, number|
|
|
121
|
+
if open
|
|
122
|
+
if source_line.strip == open[:closer]
|
|
123
|
+
if open[:runnable] && (open[:line]...number).cover?(line)
|
|
124
|
+
return [open[:offset], text.byteslice(open[:offset], offset - open[:offset]),
|
|
125
|
+
open[:line]]
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
open = nil
|
|
129
|
+
end
|
|
130
|
+
elsif source_line.start_with?("```", "<!-- doctest:boot")
|
|
131
|
+
opener = source_line.rstrip
|
|
132
|
+
open = { closer: opener.start_with?("<!--") ? "-->" : "```", runnable: RUNNABLE_OPENERS.include?(opener),
|
|
133
|
+
offset: offset + source_line.bytesize, line: number + 1 }
|
|
134
|
+
end
|
|
135
|
+
offset += source_line.bytesize
|
|
136
|
+
end
|
|
137
|
+
nil
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def collect_calls(node, line, found)
|
|
141
|
+
return unless node
|
|
142
|
+
|
|
143
|
+
if node.is_a?(Prism::CallNode) && node.arguments&.arguments&.last.is_a?(Prism::KeywordHashNode) &&
|
|
144
|
+
node.location.start_line <= line && line <= node.location.end_line
|
|
145
|
+
found << node
|
|
146
|
+
end
|
|
147
|
+
node.compact_child_nodes.each { |child| collect_calls(child, line, found) }
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def key_of(element)
|
|
151
|
+
element.is_a?(Prism::AssocNode) && element.key.is_a?(Prism::SymbolNode) ? element.key.unescaped : nil
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# The rewritten keyword arguments, or why this call does not match the plan.
|
|
155
|
+
# rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
156
|
+
def replacement_for(keywords, plan)
|
|
157
|
+
elements = keywords.elements
|
|
158
|
+
return Skip.new("a keyword with a non-literal key") if elements.any? { |e| e.is_a?(Prism::AssocNode) && key_of(e).nil? }
|
|
159
|
+
|
|
160
|
+
routing = elements.select { |element| ROUTING.map(&:to_s).include?(key_of(element)) }
|
|
161
|
+
loose = elements - routing
|
|
162
|
+
return Skip.new("no loose keywords in this call") if loose.empty?
|
|
163
|
+
return Skip.new("to: is spelled in the call but the recording routed by identity") if routing.any? do |e|
|
|
164
|
+
key_of(e) == "to"
|
|
165
|
+
end && !plan.slots.empty?
|
|
166
|
+
|
|
167
|
+
literal = loose.filter_map { |element| key_of(element) }
|
|
168
|
+
splats = loose.reject { |element| key_of(element) }
|
|
169
|
+
recorded = (plan.to_keys + plan.with_keys).uniq
|
|
170
|
+
if splats.empty? && literal.sort != recorded.sort
|
|
171
|
+
return Skip.new("the call's keys (#{literal.sort.join(', ')}) are not the recorded keys (#{recorded.sort.join(', ')})")
|
|
172
|
+
end
|
|
173
|
+
if splats.any? && !((literal - recorded).empty? && (plan.to_keys - literal).empty?)
|
|
174
|
+
return Skip.new("a splat carries keys this call does not spell out")
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
route = route_text(loose, plan)
|
|
178
|
+
return route if route.is_a?(Skip)
|
|
179
|
+
|
|
180
|
+
to_only = loose.select { |element| (plan.to_keys - plan.with_keys).include?(key_of(element)) }
|
|
181
|
+
kept = elements - to_only
|
|
182
|
+
facts = loose - to_only
|
|
183
|
+
positions = facts.map { |element| kept.index(element) }
|
|
184
|
+
return Skip.new("routing keywords sit between the facts") unless positions.each_cons(2).all? { |a, b| b == a + 1 }
|
|
185
|
+
|
|
186
|
+
assemble(keywords, kept, facts, route)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def route_text(loose, plan)
|
|
190
|
+
parts = []
|
|
191
|
+
plan.slots.each do |slot|
|
|
192
|
+
element = loose.find { |candidate| key_of(candidate) == slot["key"] }
|
|
193
|
+
expression = element && identity_expression(element.value, slot)
|
|
194
|
+
return Skip.new("#{slot['key']}: its identity is not spelled the recorded way") unless expression
|
|
195
|
+
if plan.with_keys.include?(slot["key"]) && !pure?(expression)
|
|
196
|
+
return Skip.new("#{slot['key']}: carries identity and a fact, and its expression is not safe to evaluate twice")
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
parts[slot["part"]] = expression.slice
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
case parts.size
|
|
203
|
+
when 0 then nil
|
|
204
|
+
when 1 then "to: #{parts.first}"
|
|
205
|
+
when 2 then "to: { aggregate: #{parts[0]}, entity: #{parts[1]} }"
|
|
206
|
+
else "to: { aggregate: #{parts[0]}, entities: [#{parts[1..].join(', ')}] }"
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Every fact keeps its own line, re-aligned. A fact that used to
|
|
211
|
+
# start a line was aligned under whatever the call opened with; once
|
|
212
|
+
# the facts sit inside `with: { `, that column means nothing, so each
|
|
213
|
+
# following fact is re-indented under the first fact's new column
|
|
214
|
+
# (ordinary Ruby hash alignment) and any line inside a single fact's
|
|
215
|
+
# own value is shifted by the same amount, keeping its shape.
|
|
216
|
+
def assemble(keywords, kept, facts, route)
|
|
217
|
+
elements = keywords.elements
|
|
218
|
+
source = keywords.location.slice
|
|
219
|
+
base = keywords.location.start_offset
|
|
220
|
+
gap_after = lambda do |element|
|
|
221
|
+
following = elements[elements.index(element) + 1]
|
|
222
|
+
start = element.location.end_offset - base
|
|
223
|
+
following ? source.byteslice(start, following.location.start_offset - element.location.end_offset) : ", "
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
pieces = route ? [route] : []
|
|
227
|
+
column = advance(keywords.location.start_column, pieces.empty? ? "" : "#{route}, ")
|
|
228
|
+
index = 0
|
|
229
|
+
while index < kept.size
|
|
230
|
+
element = kept[index]
|
|
231
|
+
if facts.first.equal?(element)
|
|
232
|
+
pieces << with_clause(facts, column, gap_after)
|
|
233
|
+
index += facts.size
|
|
234
|
+
else
|
|
235
|
+
pieces << element.slice
|
|
236
|
+
index += 1
|
|
237
|
+
end
|
|
238
|
+
column = advance(column, "#{pieces.last}, ")
|
|
239
|
+
end
|
|
240
|
+
pieces.join(", ")
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def with_clause(facts, column, gap_after)
|
|
244
|
+
opener = "with: { "
|
|
245
|
+
fact_column = column + opener.length
|
|
246
|
+
delta = fact_column - facts.first.location.start_column
|
|
247
|
+
body = facts.each_with_index.map do |fact, position|
|
|
248
|
+
gap = position < facts.size - 1 ? gap_after.call(fact).sub(/\n[^\S\n]*\z/, "\n#{' ' * fact_column}") : ""
|
|
249
|
+
reindent(fact.slice, delta) + gap
|
|
250
|
+
end.join
|
|
251
|
+
"#{opener}#{body} }"
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# The column `text` ends at, having started at `column`.
|
|
255
|
+
def advance(column, text)
|
|
256
|
+
before, newline, after = text.rpartition("\n")
|
|
257
|
+
newline.empty? ? column + before.length + after.length : after.length
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Every line but the first moved right (or left) by `delta`.
|
|
261
|
+
def reindent(text, delta)
|
|
262
|
+
return text if delta.zero? || !text.include?("\n")
|
|
263
|
+
|
|
264
|
+
first, _, rest = text.partition("\n")
|
|
265
|
+
"#{first}\n" + rest.gsub(/^[^\S\n]*/) do |indent|
|
|
266
|
+
delta.positive? ? indent + (" " * delta) : indent[0, [indent.length + delta, 0].max]
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def identity_expression(value, slot)
|
|
271
|
+
return value if slot["form"] == "scalar"
|
|
272
|
+
return nil unless value.is_a?(Prism::HashNode) && value.elements.one?
|
|
273
|
+
|
|
274
|
+
pair = value.elements.first
|
|
275
|
+
pair.value if key_of(pair) == slot["inner"]
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# Safe to evaluate twice: literals, variable/constant reads, and
|
|
279
|
+
# argument-free method chains over them (`order.id`, `created.instance.id`).
|
|
280
|
+
def pure?(node)
|
|
281
|
+
case node
|
|
282
|
+
when Prism::StringNode, Prism::SymbolNode, Prism::IntegerNode, Prism::FloatNode, Prism::NilNode,
|
|
283
|
+
Prism::TrueNode, Prism::FalseNode, Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode,
|
|
284
|
+
Prism::ConstantReadNode, Prism::ConstantPathNode
|
|
285
|
+
true
|
|
286
|
+
when Prism::InterpolatedStringNode
|
|
287
|
+
node.parts.all? do |part|
|
|
288
|
+
part.is_a?(Prism::StringNode) ||
|
|
289
|
+
(part.is_a?(Prism::EmbeddedStatementsNode) && Array(part.statements&.body).all? { |inner| pure?(inner) })
|
|
290
|
+
end
|
|
291
|
+
when Prism::CallNode
|
|
292
|
+
node.arguments.nil? && node.block.nil? && (node.receiver.nil? || pure?(node.receiver))
|
|
293
|
+
else
|
|
294
|
+
false
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# Nothing here requires a gem at load time — this file arrives through
|
|
2
|
+
# RUBYOPT, ahead of `bundler/setup`, and activating a default gem (json)
|
|
3
|
+
# that early makes Bundler refuse the Gemfile's own version. `json` is
|
|
4
|
+
# required on first write instead, by which point Bundler has resolved.
|
|
5
|
+
require_relative "../deprecation"
|
|
6
|
+
|
|
7
|
+
module Hecks
|
|
8
|
+
module Codemod
|
|
9
|
+
# The observing half of `bin/codemod_legacy_dispatch_args`. Loaded into a
|
|
10
|
+
# test run through RUBYOPT (`bin/codemod_legacy_dispatch_args record --
|
|
11
|
+
# <command>` arms it), it wraps `Runtime::Dispatcher#dispatch` and, for
|
|
12
|
+
# every call that passed loose keyword facts, appends one JSON line
|
|
13
|
+
# saying how that call's facts split between `to:` and `with:`.
|
|
14
|
+
#
|
|
15
|
+
# **Why record instead of reading source** — loose keyword facts carry the
|
|
16
|
+
# receiver's identity (`number: { value: "a1" }`, `id: order.id`) mixed
|
|
17
|
+
# in with the command's facts, and which key is which is a property of
|
|
18
|
+
# the target command's IR (its `identified_by`, its declared attributes,
|
|
19
|
+
# the reference aliases `ReactionInvocation` accepts), not of the call's
|
|
20
|
+
# text. The recorder asks the same `ReactionInvocation.build(explicit:
|
|
21
|
+
# true)` split a policy's `with:` projection already uses, against the
|
|
22
|
+
# real registry and the real values, then checks the strict `with:`
|
|
23
|
+
# door would accept the result. The rewriter only touches a site whose
|
|
24
|
+
# every observation agrees.
|
|
25
|
+
#
|
|
26
|
+
# Each line: site ("path:line", the caller's own line), verb, the loose
|
|
27
|
+
# keys, `to_keys` (keys that carried receiver identity), `with_keys`
|
|
28
|
+
# (declared facts — a key can be both), `slots` (which key fills which
|
|
29
|
+
# part of the route, and whether its identity sat inside a one-field
|
|
30
|
+
# hash), `strict` (the refusal class strict `with:` raises, or nil),
|
|
31
|
+
# `outcome` ("ok" or the class the real dispatch raised),
|
|
32
|
+
# `payload_keys` (every key the dispatch's own events carried), and
|
|
33
|
+
# `unrewritable` when no split exists.
|
|
34
|
+
module LegacyDispatchRecorder
|
|
35
|
+
module_function
|
|
36
|
+
|
|
37
|
+
def arm!(path)
|
|
38
|
+
return install!(path) if defined?(Hecks::Runtime::Dispatcher)
|
|
39
|
+
|
|
40
|
+
trace = TracePoint.new(:end) do |point|
|
|
41
|
+
next unless point.self.is_a?(Module) && point.self.name == "Hecks::Runtime::Dispatcher"
|
|
42
|
+
|
|
43
|
+
trace.disable
|
|
44
|
+
install!(path)
|
|
45
|
+
end
|
|
46
|
+
trace.enable
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def install!(path)
|
|
50
|
+
Hecks::Runtime::Dispatcher.prepend(Wrap)
|
|
51
|
+
Wrap.path = path
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Prepended onto Runtime::Dispatcher.
|
|
55
|
+
module Wrap
|
|
56
|
+
class << self
|
|
57
|
+
attr_accessor :path
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def dispatch(verb, to: nil, with: nil, saga_correlation: nil, **legacy_args)
|
|
61
|
+
return super if legacy_args.empty?
|
|
62
|
+
|
|
63
|
+
entry = LegacyDispatchRecorder.observe(self, verb, to, with, legacy_args)
|
|
64
|
+
begin
|
|
65
|
+
result = super
|
|
66
|
+
entry[:outcome] = "ok"
|
|
67
|
+
entry[:payload_keys] = LegacyDispatchRecorder.payload_keys(result)
|
|
68
|
+
result
|
|
69
|
+
rescue Exception => e # rubocop:disable Lint/RescueException -- recorded, then re-raised unchanged
|
|
70
|
+
entry[:outcome] = e.class.name
|
|
71
|
+
raise
|
|
72
|
+
ensure
|
|
73
|
+
LegacyDispatchRecorder.write(Wrap.path, entry)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Every key the dispatch's own events carry. A loose fact reaches the
|
|
79
|
+
# event payload whether the command declares it or not, and a policy
|
|
80
|
+
# with no `with:` projection forwards that payload verbatim — so
|
|
81
|
+
# moving a key out of the facts and into `to:` can silently empty a
|
|
82
|
+
# field whatever reacts downstream needs. Recorded here so `plan_for`
|
|
83
|
+
# can refuse exactly those sites; a real one (Banking's own
|
|
84
|
+
# FreezeAccount -> ReviewOnFreeze -> AccountFreezeReview.Open, in
|
|
85
|
+
# docs/implemented/reference/policy.md, whose own `# => true` on
|
|
86
|
+
# `reaction_log.last[:delivered]` went false) is why this exists.
|
|
87
|
+
def payload_keys(result)
|
|
88
|
+
return [] unless result.respond_to?(:events)
|
|
89
|
+
|
|
90
|
+
result.events.flat_map { |event| Hash(event.payload).keys.map(&:to_s) }.uniq
|
|
91
|
+
rescue StandardError
|
|
92
|
+
[]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def observe(dispatcher, verb, to, with, legacy)
|
|
96
|
+
frame = caller_locations(2).find { |location| Hecks::Deprecation.external?(location) }
|
|
97
|
+
entry = { site: frame && "#{frame.absolute_path || frame.path}:#{frame.lineno}", verb: verb.to_s,
|
|
98
|
+
to_given: !to.nil?, keys: legacy.keys.map(&:to_s) }
|
|
99
|
+
entry.merge(split(dispatcher.registry, verb, to, with, legacy))
|
|
100
|
+
rescue StandardError => e
|
|
101
|
+
entry.merge(unrewritable: "#{e.class}: #{e.message}")
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def write(path, entry)
|
|
105
|
+
require "json"
|
|
106
|
+
File.open(path, "a") do |file|
|
|
107
|
+
file.flock(File::LOCK_EX)
|
|
108
|
+
file.puts(JSON.generate(entry))
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def split(registry, verb, to, with, legacy)
|
|
113
|
+
return { unrewritable: "with: and loose keywords in the same call" } if with
|
|
114
|
+
return { unrewritable: "a loose keyword that is not a Symbol" } unless legacy.keys.all?(Symbol)
|
|
115
|
+
|
|
116
|
+
invocation = Hecks::Runtime::ReactionInvocation
|
|
117
|
+
target = invocation.send(:resolve_target, registry, verb)
|
|
118
|
+
|
|
119
|
+
if to
|
|
120
|
+
return { to_keys: [], with_keys: legacy.keys.map(&:to_s), slots: [],
|
|
121
|
+
strict: strict(target, to, legacy) }
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
built = invocation.build(registry: registry, verb: verb, projected: legacy, explicit: true)
|
|
125
|
+
route = built[:to]
|
|
126
|
+
with_keys = legacy.keys.select { |key| built[:with].key?(key) }
|
|
127
|
+
to_keys = route.nil? ? [] : legacy.keys.select { |key| consumed?(registry, verb, legacy, key, route) }
|
|
128
|
+
slots = slots_for(route, to_keys, legacy)
|
|
129
|
+
return slots if slots.is_a?(Hash)
|
|
130
|
+
|
|
131
|
+
{ to_keys: to_keys.map(&:to_s), with_keys: with_keys.map(&:to_s), slots: slots,
|
|
132
|
+
strict: strict(target, route, legacy.slice(*with_keys)) }
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# A key carried receiver identity when the route changes (or can no
|
|
136
|
+
# longer be built) without it.
|
|
137
|
+
def consumed?(registry, verb, legacy, key, route)
|
|
138
|
+
Hecks::Runtime::ReactionInvocation.build(registry: registry, verb: verb,
|
|
139
|
+
projected: legacy.except(key), explicit: true)[:to] != route
|
|
140
|
+
rescue StandardError
|
|
141
|
+
true
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Which key fills each part of the route. Refuses anything a source
|
|
145
|
+
# rewrite cannot spell as `to: <that key's expression>`: a composite
|
|
146
|
+
# identity, two keys naming the same part, or a non-String identity
|
|
147
|
+
# (`to:` takes only a String).
|
|
148
|
+
def slots_for(route, to_keys, legacy)
|
|
149
|
+
return [] if route.nil?
|
|
150
|
+
|
|
151
|
+
parts = route.is_a?(Hash) ? [route[:aggregate], *route[:entities]] : [route]
|
|
152
|
+
if to_keys.size != parts.size
|
|
153
|
+
return { unrewritable: "identity spread over #{to_keys.size} keys for #{parts.size} route parts" }
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
to_keys.map do |key|
|
|
157
|
+
form, inner, text = candidate(legacy[key])
|
|
158
|
+
return { unrewritable: "#{key}: identity is not a String or a one-field hash of one" } unless text
|
|
159
|
+
|
|
160
|
+
matches = parts.each_index.select { |index| parts[index].to_s == text }
|
|
161
|
+
return { unrewritable: "#{key}: identity matches #{matches.size} route parts" } unless matches.one?
|
|
162
|
+
|
|
163
|
+
{ key: key.to_s, part: matches.first, form: form, inner: inner }
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def candidate(raw)
|
|
168
|
+
return ["scalar", nil, raw] if raw.is_a?(String)
|
|
169
|
+
return nil unless raw.is_a?(Hash) && raw.size == 1
|
|
170
|
+
|
|
171
|
+
inner, value = raw.first
|
|
172
|
+
["hash", inner.to_s, value] if value.is_a?(String)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def strict(target, route, facts)
|
|
176
|
+
Hecks::Runtime::Invocation.route(route, entity_depth: target.entities.size)
|
|
177
|
+
Hecks::Runtime::Invocation.facts_for(target.command, with: facts, legacy: {})
|
|
178
|
+
nil
|
|
179
|
+
rescue StandardError => e
|
|
180
|
+
e.class.name
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
Hecks::Codemod::LegacyDispatchRecorder.arm!(ENV["HECKS_LEGACY_DISPATCH_RECORD"]) if ENV["HECKS_LEGACY_DISPATCH_RECORD"]
|