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
|
@@ -1,17 +1,19 @@
|
|
|
1
|
+
require "hecks/vocabulary"
|
|
2
|
+
|
|
1
3
|
module Hecks
|
|
2
4
|
module Bluebook
|
|
3
5
|
# Lightweight formal methods over the IR — the same family as TLA+/
|
|
4
6
|
# Alloy/P: every lifecycle is a declared finite state machine and
|
|
5
|
-
# every process manager a declared protocol, and
|
|
6
|
-
# data, not code, so they can be
|
|
7
|
+
# every process manager a declared protocol, and both are already
|
|
8
|
+
# data, not code, so they can be model-checked rather than merely
|
|
7
9
|
# executed. Static analysis only — no bluebook boots twice, no
|
|
8
10
|
# runtime is touched — over what meta_validator/judge.rb and the
|
|
9
11
|
# builders' own `validate!` methods leave uncovered (an undeclared
|
|
10
12
|
# transition target, a dispatch to nowhere, a compensation nothing
|
|
11
13
|
# can ever reach).
|
|
12
14
|
#
|
|
13
|
-
#
|
|
14
|
-
# A checker over TLA+ verifies a
|
|
15
|
+
# **The rare property this rests on**: the model is the implementation.
|
|
16
|
+
# A checker over TLA+ verifies a spec a human keeps in sync with
|
|
15
17
|
# code by hand ; this verifies the same IR the runtime dispatches
|
|
16
18
|
# against, so there is no second copy to drift.
|
|
17
19
|
module ModelCheck
|
|
@@ -19,8 +21,8 @@ module Hecks
|
|
|
19
21
|
def to_s = "#{severity.to_s.upcase.ljust(7)} #{kind.to_s.ljust(20)} #{subject} — #{message}"
|
|
20
22
|
end
|
|
21
23
|
|
|
22
|
-
# A
|
|
23
|
-
# allowlists enforced
|
|
24
|
+
# **A finding shipped, not silenced** — the coverage-gate idiom, empty
|
|
25
|
+
# allowlists enforced both directions (spec/model_check_spec.rb holds
|
|
24
26
|
# this exact table: an error the checker reports and this does not
|
|
25
27
|
# name is a regression, an entry the checker no longer reports is
|
|
26
28
|
# stale and must be deleted). bin/model_check reads this same
|
|
@@ -29,42 +31,73 @@ module Hecks
|
|
|
29
31
|
# "banking"/ExternalSettlement — found on the first real run:
|
|
30
32
|
# ExternalSettlement declares `ends_on "ExternalTransferSent"`, and
|
|
31
33
|
# ExternalTransfer.Send genuinely emits it — the event is real, and
|
|
32
|
-
# the
|
|
33
|
-
# the
|
|
34
|
+
# the aggregate reaches "sent" (its own, separate lifecycle) — but
|
|
35
|
+
# the saga's protocol has no `on "ExternalTransferSent"` handler, so
|
|
34
36
|
# its own `state "sent"` is unreachable through the chain the
|
|
35
37
|
# checker walks, and the saga's own bookkeeping (saga_log, ends_on)
|
|
36
38
|
# never closes it. Real domain activity is unaffected; the saga's
|
|
37
|
-
#
|
|
39
|
+
# own tracking of it is not. Left named rather than redesigning a
|
|
38
40
|
# corpus fixture that is not this checker's to redesign.
|
|
39
41
|
# S7, ADR 0025 — the ExternalSettlement finding this used to
|
|
40
|
-
# allowlist is
|
|
42
|
+
# allowlist is gone, not just quieted: its "sent" state was a
|
|
41
43
|
# `state "x"` line never named by any handler's own from:/to:, a
|
|
42
|
-
# pure declaration-drift artifact. States are
|
|
44
|
+
# pure declaration-drift artifact. States are derived from the
|
|
43
45
|
# transitions that name them now (ProcessManagerBuilder#derived_
|
|
44
46
|
# states), so a state nothing ever transitions into or out of no
|
|
45
47
|
# longer exists to be unreachable — the finding this allowlisted
|
|
46
48
|
# cannot occur any more, by construction.
|
|
47
49
|
#
|
|
48
|
-
# "banking"/NotifyOnClosure, FlagKeyReturn —
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
#
|
|
50
|
+
# "banking"/NotifyOnClosure, FlagKeyReturn — gone from here, moved
|
|
51
|
+
# to banking. `across "Notifications"` names a domain that does not
|
|
52
|
+
# exist anywhere in this repo, deliberately (it exercises the
|
|
53
|
+
# undelivered-reaction runtime path — `spec/runtime/policy_spec.rb`,
|
|
54
|
+
# "records a reaction it cannot deliver rather than swallowing it").
|
|
55
|
+
# That expectation is now declared on the two policies themselves —
|
|
56
|
+
# `across "Notifications", expect_undelivered: true` — and
|
|
57
|
+
# `expected_undelivered_findings` below holds it in both directions:
|
|
58
|
+
# the unknown-target and unacknowledged-relationship findings are
|
|
59
|
+
# expected, and a declaration whose target turns out reachable is a
|
|
60
|
+
# `stale_undelivered_expectation` error. A domain's own allowance
|
|
61
|
+
# lives in its own source, never in a core table keyed by its name.
|
|
62
|
+
#
|
|
63
|
+
# Pinned empty (spec/model_check_spec.rb), the way `bin/fuzz`'s
|
|
64
|
+
# `KNOWN_FUZZ_FINDINGS` is: a finding a domain means to keep belongs
|
|
65
|
+
# in that domain's own declaration.
|
|
61
66
|
ALLOWED_FINDINGS = {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
# QualityControl was the first domain in this corpus to trigger an
|
|
68
|
+
# `asks`/`tells` port operation from a `policy`, and used to carry
|
|
69
|
+
# two entries here for it — both gone now, not just quieted:
|
|
70
|
+
#
|
|
71
|
+
# `deaf_policy` (ClearOnPass, RefuseOnFail, RecordTheIssue,
|
|
72
|
+
# RecordTheRefusal) went first: `emitted_events` below now reads an
|
|
73
|
+
# outbound operation's `.answers`/`.refuses` the same way it already
|
|
74
|
+
# read a command's `.emits`, so `Clearance.SuitePassed`/`SuiteFailed`
|
|
75
|
+
# and `Ticket.IssueFiled`/`IssueFilingRefused` enter the known-emits
|
|
76
|
+
# set for real — the same fix `bin/qa_pr_check`'s own move to
|
|
77
|
+
# dispatching through the CI port (rather than `Clearance::Passed`/
|
|
78
|
+
# `Failed` directly) needed to make these two policies actually fire.
|
|
79
|
+
#
|
|
80
|
+
# `unknown_trigger` (FileWhenSubmitted, AskOnceMore) — BUG#23 — was
|
|
81
|
+
# never actually a `Naming`/`PolicyBuilder` defect, confirmed by
|
|
82
|
+
# tracing the real dispatch path rather than assuming the comment
|
|
83
|
+
# that used to sit here: `Naming.command_ref`'s bare-constant
|
|
84
|
+
# rewrite does leave `trigger Ticket::IssueTracker::File` (aggregate,
|
|
85
|
+
# port, operation) as "Ticket::IssueTracker.File", a leftover `::`
|
|
86
|
+
# past the aggregate — but `PolicyInterpreter#deliver` re-qualifies
|
|
87
|
+
# every trigger with this domain's own name before dispatch
|
|
88
|
+
# ("QualityControl::Ticket::IssueTracker.File"), and `Naming.
|
|
89
|
+
# split_verb` already folds that reintroduced `::` into the
|
|
90
|
+
# dot-joined tail correctly (fixed for `ReactionInvocation#
|
|
91
|
+
# resolve_target`, PR #520, predating this entry's own removal) —
|
|
92
|
+
# confirmed live: a real dispatch through `Ticket.Submit` fires
|
|
93
|
+
# `IssueFiled`/`TicketFiled` exactly as declared. The actual gap was
|
|
94
|
+
# entirely in this checker: `verbs_of` never enumerated a port
|
|
95
|
+
# operation as a triggerable verb at all, and `policy_findings`
|
|
96
|
+
# compared raw strings instead of `Naming.split_verb` triples the
|
|
97
|
+
# way `handler_findings`'s own `unknown_dispatch` check already does
|
|
98
|
+
# (BUG#6). Fixed with `port_verbs_of`/`triggerable_verbs`, scoped
|
|
99
|
+
# entirely to this file — no change to `Naming` or `PolicyBuilder`
|
|
100
|
+
# was needed or made.
|
|
68
101
|
}.freeze
|
|
69
102
|
|
|
70
103
|
module_function
|
|
@@ -72,16 +105,20 @@ module Hecks
|
|
|
72
105
|
# `hecksagon:`/`known_domains:` — both optional, both `nil`-safe
|
|
73
106
|
# (every existing caller with no sibling hecksagon, or checking one
|
|
74
107
|
# domain in isolation, behaves exactly as before). `hecksagon` is
|
|
75
|
-
#
|
|
108
|
+
# this bluebook's own sibling wiring file, if the caller loaded one
|
|
76
109
|
# (see `emitted_events`'s own comment on why a caller that didn't
|
|
77
110
|
# simply finds none, correctly). `known_domains` is the caller's
|
|
78
|
-
#
|
|
111
|
+
# own corpus-wide view — every bluebook/hecksagon name it has
|
|
79
112
|
# booted anywhere, across every domain it has looked at, not just
|
|
80
113
|
# this one — used only to catch a typo'd `across`/`uses_framework`
|
|
81
114
|
# target; see `cross_domain_policy_findings`'s own comment for why
|
|
82
115
|
# this can only ever be a corpus-scoped heuristic, never a general
|
|
83
116
|
# correctness guarantee.
|
|
84
|
-
|
|
117
|
+
#
|
|
118
|
+
# `rust_target:`/`strict:` — both default false, both only change the
|
|
119
|
+
# severity of `rust_reserved_name` findings (see
|
|
120
|
+
# `rust_reserved_name_findings`); every other finding is unaffected.
|
|
121
|
+
def call(bluebook, hecksagon: nil, known_domains: nil, rust_target: false, strict: false)
|
|
85
122
|
findings = []
|
|
86
123
|
bluebook.aggregates.each do |aggregate|
|
|
87
124
|
findings.concat(lifecycle_findings(aggregate, aggregate))
|
|
@@ -89,10 +126,68 @@ module Hecks
|
|
|
89
126
|
end
|
|
90
127
|
bluebook.process_managers.each { |process_manager| findings.concat(saga_findings(bluebook, process_manager)) }
|
|
91
128
|
bluebook.policies.each { |policy| findings.concat(policy_findings(bluebook, policy, hecksagon, known_domains)) }
|
|
129
|
+
findings.concat(rust_reserved_name_findings(domain_name: bluebook.name,
|
|
130
|
+
aggregate_names: bluebook.aggregates.map(&:hecks_name),
|
|
131
|
+
rust_target: rust_target, strict: strict))
|
|
92
132
|
findings
|
|
93
133
|
end
|
|
94
134
|
|
|
95
|
-
# ──
|
|
135
|
+
# ── Rust reserved names ───────────────────────────────────────────
|
|
136
|
+
#
|
|
137
|
+
# A name that becomes a bare Rust module identifier with no `r#`
|
|
138
|
+
# escape hatch: an aggregate (`pub mod <name.downcase>;` plus its
|
|
139
|
+
# `<name.downcase>.rs` file) and a domain (`pub mod <name>;` and a
|
|
140
|
+
# Cargo `[features]` key). Field names are not checked — both
|
|
141
|
+
# generators already raw-escape those (`rust_ident_field`).
|
|
142
|
+
#
|
|
143
|
+
# The words come from the `RustReservedWord`/`CargoReservedName`
|
|
144
|
+
# vocabularies, the same tables `rust/project/naming.rb` and
|
|
145
|
+
# hecks-codegen's generated `reserved_names.rs` read. Both Rust
|
|
146
|
+
# generators refuse through this check (`Projector.
|
|
147
|
+
# reserved_name_refusal`, and its hecks-codegen port in `naming.rs`).
|
|
148
|
+
#
|
|
149
|
+
# Severity: a domain that only ever runs in Ruby is fine with an
|
|
150
|
+
# aggregate named `Match`, so this warns by default. It is an error
|
|
151
|
+
# when the caller says the domain has a Rust target (`rust_target:` —
|
|
152
|
+
# `bin/model_check` reads it off the domain's Cargo feature, the
|
|
153
|
+
# generators always pass it) or asks for strictness (`strict:`,
|
|
154
|
+
# `bin/model_check --strict`).
|
|
155
|
+
#
|
|
156
|
+
# The module-name transform is `downcase`, the one both generators
|
|
157
|
+
# apply to an aggregate name and to an attached chapter's name.
|
|
158
|
+
def rust_reserved_name_findings(domain_name: nil, aggregate_names: [], rust_target: false, strict: false)
|
|
159
|
+
severity = rust_target || strict ? :error : :warning
|
|
160
|
+
keywords = Hecks::Vocabulary.fetch("RustReservedWord")
|
|
161
|
+
|
|
162
|
+
findings = aggregate_names.filter_map do |name|
|
|
163
|
+
module_name = rust_module_name(name)
|
|
164
|
+
next unless keywords.include?(module_name)
|
|
165
|
+
|
|
166
|
+
Finding.new(kind: :rust_reserved_name, severity: severity, subject: name.to_s,
|
|
167
|
+
message: "the aggregate's Rust module `#{module_name}` is a Rust keyword (RustReservedWord) — " \
|
|
168
|
+
"`pub mod #{module_name};` has no raw-identifier escape; rename the aggregate")
|
|
169
|
+
end
|
|
170
|
+
findings.concat(domain_reserved_name_findings(domain_name, keywords, severity)) if domain_name
|
|
171
|
+
findings
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def domain_reserved_name_findings(domain_name, keywords, severity)
|
|
175
|
+
module_name = rust_module_name(domain_name)
|
|
176
|
+
table = if keywords.include?(module_name)
|
|
177
|
+
"a Rust keyword (RustReservedWord)"
|
|
178
|
+
elsif Hecks::Vocabulary.fetch("CargoReservedName").include?(module_name)
|
|
179
|
+
"a reserved Cargo.toml key (CargoReservedName)"
|
|
180
|
+
end
|
|
181
|
+
return [] unless table
|
|
182
|
+
|
|
183
|
+
[Finding.new(kind: :rust_reserved_name, severity: severity, subject: domain_name.to_s,
|
|
184
|
+
message: "the domain's Rust module and Cargo feature `#{module_name}` is #{table} — " \
|
|
185
|
+
"rename the domain")]
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def rust_module_name(name) = name.to_s.downcase
|
|
189
|
+
|
|
190
|
+
# ── lifecycles (aggregate and entity — a piece may declare one too) ──
|
|
96
191
|
|
|
97
192
|
def lifecycle_findings(aggregate, declaring)
|
|
98
193
|
lifecycle = declaring.lifecycle
|
|
@@ -164,7 +259,7 @@ module Hecks
|
|
|
164
259
|
).uniq
|
|
165
260
|
end
|
|
166
261
|
|
|
167
|
-
# Least fixpoint from the default state: an
|
|
262
|
+
# Least fixpoint from the default state: an unconstrained
|
|
168
263
|
# transition always fires, from wherever the machine is ; a
|
|
169
264
|
# constrained one fires once any of its named sources is reached.
|
|
170
265
|
def reachable_states(lifecycle)
|
|
@@ -188,8 +283,8 @@ module Hecks
|
|
|
188
283
|
reached
|
|
189
284
|
end
|
|
190
285
|
|
|
191
|
-
# A state with no
|
|
192
|
-
# stuck-state
|
|
286
|
+
# A state with no outgoing declared path at all is exempt from the
|
|
287
|
+
# stuck-state warning for a different reason than "it fires an
|
|
193
288
|
# unconstrained transition" — the default state of a lifecycle
|
|
194
289
|
# with only constrained transitions is legitimately allowed to sit
|
|
195
290
|
# forever, since nothing about *entering* it via default implies
|
|
@@ -197,7 +292,7 @@ module Hecks
|
|
|
197
292
|
# explicitly delivered somewhere.
|
|
198
293
|
#
|
|
199
294
|
# Scoped to `default` alone, and only when the lifecycle actually
|
|
200
|
-
# declares real transitions elsewhere: an
|
|
295
|
+
# declares real transitions elsewhere: an empty lifecycle (no
|
|
201
296
|
# transitions at all) doesn't get this exemption — that's not "a
|
|
202
297
|
# machine whose entry point deliberately awaits external action,"
|
|
203
298
|
# it's much more likely a lifecycle nobody finished wiring, and
|
|
@@ -215,7 +310,12 @@ module Hecks
|
|
|
215
310
|
|
|
216
311
|
def saga_findings(bluebook, process_manager)
|
|
217
312
|
emitted = emitted_events(bluebook)
|
|
218
|
-
|
|
313
|
+
# (domain, aggregate, command) triples, not raw strings — see
|
|
314
|
+
# `handler_findings`'s own comment on the dispatch side for why:
|
|
315
|
+
# `Naming.split_verb` is what makes an entity verb's two legitimate
|
|
316
|
+
# spellings (`Naming.command_ref`'s own `::`-then-`.` rewrite vs.
|
|
317
|
+
# `verbs_of`'s own all-`.` one) compare equal.
|
|
318
|
+
verbs = verbs_of(bluebook).map { |verb| Naming.split_verb(verb) }
|
|
219
319
|
reached = pm_reachable_states(process_manager, emitted)
|
|
220
320
|
|
|
221
321
|
findings = []
|
|
@@ -246,11 +346,11 @@ module Hecks
|
|
|
246
346
|
end
|
|
247
347
|
end
|
|
248
348
|
|
|
249
|
-
#
|
|
349
|
+
# One handler's own findings — deaf_handler, unknown_dispatch (one
|
|
250
350
|
# per dispatch), and unarmed_compensation (one per compensating
|
|
251
351
|
# dispatch), pulled out of saga_findings' own handler loop; each
|
|
252
352
|
# check reads only this handler plus the domain-wide emitted/verbs
|
|
253
|
-
# sets saga_findings already resolved once, no state shared
|
|
353
|
+
# sets saga_findings already resolved once, no state shared between
|
|
254
354
|
# handlers.
|
|
255
355
|
def handler_findings(bluebook, process_manager, emitted, verbs, handler)
|
|
256
356
|
findings = []
|
|
@@ -266,16 +366,38 @@ module Hecks
|
|
|
266
366
|
end
|
|
267
367
|
|
|
268
368
|
handler.dispatches.each do |dispatch|
|
|
269
|
-
#
|
|
270
|
-
#
|
|
271
|
-
#
|
|
272
|
-
#
|
|
273
|
-
#
|
|
274
|
-
qualified
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
369
|
+
# **Always this domain** — same fix, same reason, as `SagaInterpreter
|
|
370
|
+
# #qualified` (BUG#6). This used to guess: a dispatch whose own
|
|
371
|
+
# `command_name` still carried a leftover `::` after `Naming.
|
|
372
|
+
# command_ref`'s own rewrite was read as "already qualified" and
|
|
373
|
+
# left alone — the exact same string-shape ambiguity that
|
|
374
|
+
# `SagaInterpreter#qualified`'s own comment explains at length
|
|
375
|
+
# (a same-domain entity command reference and a genuinely
|
|
376
|
+
# cross-domain one are textually indistinguishable after that
|
|
377
|
+
# rewrite). Confirmed against the entire corpus, same as that
|
|
378
|
+
# fix: no saga anywhere ever dispatches genuinely cross-domain,
|
|
379
|
+
# so this checker now qualifies exactly the way the runtime
|
|
380
|
+
# actually dispatches — unconditionally against `bluebook.name`
|
|
381
|
+
# — instead of maintaining its own, independently-wrong copy of
|
|
382
|
+
# the same guess.
|
|
383
|
+
#
|
|
384
|
+
# Compared as a triple, not a string — `Naming.command_ref`'s
|
|
385
|
+
# own rewrite of an entity reference (`Manifest::Slot::Fill`)
|
|
386
|
+
# collapses to "Manifest::Slot.Fill" (`::` between aggregate and
|
|
387
|
+
# entity, `.` before the command); `verbs_of`'s own entity
|
|
388
|
+
# spelling, below, joins aggregate/entity/command all with `.`
|
|
389
|
+
# instead (matching `fuzzing/sequence_generator/catalog.rb`'s own
|
|
390
|
+
# independent convention, its comment's own "the same spelling"
|
|
391
|
+
# claim). Both are legitimate, and `Naming.split_verb` already
|
|
392
|
+
# parses either to the identical (domain, aggregate, command)
|
|
393
|
+
# triple (its own comment: "past the already-resolved domain
|
|
394
|
+
# boundary, any leftover `::` is unambiguous... folding it into
|
|
395
|
+
# the dot-joined tail") — the same reading `ReactionInvocation.
|
|
396
|
+
# resolve_target` relies on at runtime. A bare string `include?`
|
|
397
|
+
# would falsely flag every entity dispatch as unknown_dispatch
|
|
398
|
+
# even once correctly domain-qualified, comparing two spellings
|
|
399
|
+
# of the same verb as though they were different ones.
|
|
400
|
+
qualified = Naming.split_verb("#{bluebook.name}::#{dispatch.command_name}")
|
|
279
401
|
next if verbs.include?(qualified)
|
|
280
402
|
|
|
281
403
|
findings << Finding.new(kind: :unknown_dispatch, severity: :error, subject: process_manager.name,
|
|
@@ -284,7 +406,7 @@ module Hecks
|
|
|
284
406
|
"checker's scope, same as CommandRules#resolve_references")
|
|
285
407
|
end
|
|
286
408
|
|
|
287
|
-
# A `compensates`
|
|
409
|
+
# A `compensates` declared with nowhere to ever fire — the exact
|
|
288
410
|
# shape of the real bug this whole feature closes ("the
|
|
289
411
|
# reversal was written and never armed"), caught at build/
|
|
290
412
|
# model-check time instead of discovered in production. No
|
|
@@ -313,7 +435,7 @@ module Hecks
|
|
|
313
435
|
end
|
|
314
436
|
|
|
315
437
|
# A handler edge is only usable in the closure if it can actually
|
|
316
|
-
#
|
|
438
|
+
# fire — REFUSED always can (it is a compensation trigger, not an
|
|
317
439
|
# event), and any other handler needs its event genuinely emitted.
|
|
318
440
|
# Without this, a deaf handler's declared from_state -> to_state
|
|
319
441
|
# pair reads as connected even though nothing can ever traverse
|
|
@@ -347,8 +469,8 @@ module Hecks
|
|
|
347
469
|
emitted = emitted_events(bluebook)
|
|
348
470
|
findings = []
|
|
349
471
|
|
|
350
|
-
# `policy.event_name` (Naming.unqualified) —
|
|
351
|
-
# strips a "::" domain qualifier. An
|
|
472
|
+
# `policy.event_name` (Naming.unqualified) — not `bare`, which only
|
|
473
|
+
# strips a "::" domain qualifier. An aggregate-scoped policy's
|
|
352
474
|
# `on_event` carries a "." aggregate qualifier instead (PolicyBuilder
|
|
353
475
|
# stores whatever was typed, verbatim — see `on "Account.
|
|
354
476
|
# AccountFrozen"`), and `bare` left it untouched, silently comparing
|
|
@@ -366,9 +488,27 @@ module Hecks
|
|
|
366
488
|
# `trigger` is spelled "Aggregate.Command" (or "Entity.Command" one
|
|
367
489
|
# level down), completed to an FQN by PolicyInterpreter#deliver as
|
|
368
490
|
# "#{domain}::#{trigger_command}" — the same join `verbs_of` builds
|
|
369
|
-
# independently, so the two spellings have to be compared as FQNs
|
|
370
|
-
#
|
|
371
|
-
|
|
491
|
+
# independently, so the two spellings have to be compared as FQNs.
|
|
492
|
+
#
|
|
493
|
+
# Compared as a triple, not a string — `handler_findings`'s own
|
|
494
|
+
# `unknown_dispatch` check (BUG#6) already applies this fix for a
|
|
495
|
+
# saga's dispatch; a policy's `trigger` needed the identical one. A
|
|
496
|
+
# policy triggering an `asks`/`tells` port operation (`Aggregate::
|
|
497
|
+
# Port::Operation`, three colon-joined segments — `Naming.command_ref`'s
|
|
498
|
+
# bare-constant rewrite turns this into "Aggregate::Port.Operation",
|
|
499
|
+
# a leftover `::` past the aggregate) is a real, working dispatch —
|
|
500
|
+
# `PolicyInterpreter#deliver` qualifies it with this domain's own
|
|
501
|
+
# name before `Naming.split_verb` ever sees it, and `split_verb`
|
|
502
|
+
# already folds that leftover `::` into the dot-joined tail
|
|
503
|
+
# correctly (fixed for `ReactionInvocation#resolve_target`, PR
|
|
504
|
+
# #520) — but this check compared raw strings against `verbs_of`,
|
|
505
|
+
# which never enumerated port operations at all, so it reported
|
|
506
|
+
# every port-operation trigger as unknown regardless. `triggerable_
|
|
507
|
+
# verbs` now includes both, and both sides are parsed through
|
|
508
|
+
# `Naming.split_verb` before comparing, the same reading
|
|
509
|
+
# `resolve_target` relies on at runtime.
|
|
510
|
+
qualified = Naming.split_verb("#{bluebook.name}::#{policy.trigger_command}")
|
|
511
|
+
unless qualified && triggerable_verbs(bluebook).include?(qualified)
|
|
372
512
|
findings << Finding.new(kind: :unknown_trigger, severity: :error, subject: policy.name,
|
|
373
513
|
message: "trigger #{policy.trigger_command.inspect} resolves to no command " \
|
|
374
514
|
"this domain declares")
|
|
@@ -379,17 +519,17 @@ module Hecks
|
|
|
379
519
|
|
|
380
520
|
# ── cross-domain policies (Context Mapping) ───────────────────────
|
|
381
521
|
#
|
|
382
|
-
# `uses_framework "X"` already
|
|
383
|
-
# merges X's own bluebook into
|
|
384
|
-
# domain `policy ... across: "X"` already
|
|
522
|
+
# `uses_framework "X"` already is a Shared Kernel relationship — it
|
|
523
|
+
# merges X's own bluebook into this registry, no boundary. A cross-
|
|
524
|
+
# domain `policy ... across: "X"` already is a Customer/Supplier
|
|
385
525
|
# relationship — it dispatches into X over real cross-Lambda RPC in
|
|
386
526
|
# the Rust host (`rust/host/src/lambda_client.rs`). Neither is a new
|
|
387
|
-
# word; this makes the
|
|
527
|
+
# word; this makes the choice between them checked instead of a
|
|
388
528
|
# prose comment nobody enforces (`examples/banking/bluebook/
|
|
389
529
|
# banking.hecksagon`'s own hand-written note explaining why
|
|
390
530
|
# Compliance is reached via `across`, never `uses_framework`).
|
|
391
531
|
#
|
|
392
|
-
#
|
|
532
|
+
# No new keyword anywhere — ADR 0025 principle 1 ("one idea, one
|
|
393
533
|
# spelling") refuses a `relationship:`/`as:` argument that would
|
|
394
534
|
# just restate, as a string, the fact the chosen keyword (
|
|
395
535
|
# `uses_framework` vs `across`) already states completely. The
|
|
@@ -397,14 +537,15 @@ module Hecks
|
|
|
397
537
|
# the finding's own name and this comment, and in prose docs — not
|
|
398
538
|
# in the grammar.
|
|
399
539
|
def cross_domain_policy_findings(policy, hecksagon, known_domains)
|
|
540
|
+
return expected_undelivered_findings(policy, hecksagon, known_domains) if policy.expect_undelivered
|
|
400
541
|
return [] unless hecksagon # no sibling hecksagon loaded — nothing to check a relationship against.
|
|
401
542
|
|
|
402
543
|
target = policy.target_domain
|
|
403
544
|
findings = []
|
|
404
545
|
|
|
405
546
|
if hecksagon.framework_members.include?(target)
|
|
406
|
-
#
|
|
407
|
-
#
|
|
547
|
+
# Shared kernel and customer/supplier are mutually exclusive
|
|
548
|
+
# claims about the same target — `uses_framework` means "X is
|
|
408
549
|
# loaded in-process, right here"; `across` means "X is a
|
|
409
550
|
# separate deployment, reached only by RPC." Declaring both is
|
|
410
551
|
# either a pointless RPC to a domain already local, or a
|
|
@@ -416,7 +557,7 @@ module Hecks
|
|
|
416
557
|
"relationship declarations contradict each other for the same " \
|
|
417
558
|
"target domain")
|
|
418
559
|
elsif hecksagon.subscriptions.none? { |subscribed| Naming.qualifier(subscribed) == target }
|
|
419
|
-
#
|
|
560
|
+
# This is what finally gives `subscribe` real teeth — checked
|
|
420
561
|
# here, at model-check time, still never routed at runtime
|
|
421
562
|
# (nothing dispatches off a `subscribe` line; see hecksagon.md's
|
|
422
563
|
# own "checked, not routed" section). ADR 0025 names `subscribe`
|
|
@@ -429,50 +570,97 @@ module Hecks
|
|
|
429
570
|
"attach it in-process instead")
|
|
430
571
|
end
|
|
431
572
|
|
|
432
|
-
#
|
|
433
|
-
# ever be a
|
|
573
|
+
# **Typo detection, deliberately weaker** — `known_domains` can only
|
|
574
|
+
# ever be a monorepo-scoped heuristic: a real external hecks
|
|
434
575
|
# consumer's own domain (this repo's own embryonaut/lifeadelics-
|
|
435
576
|
# shaped case) lives in a genuinely separate repository this
|
|
436
577
|
# corpus scan can never see, so a target this check cannot find
|
|
437
|
-
# is "unknown to THIS corpus," never proof of a typo.
|
|
438
|
-
#
|
|
439
|
-
# undefined by design (the corpus's own "Notifications," used
|
|
578
|
+
# is "unknown to THIS corpus," never proof of a typo. A target
|
|
579
|
+
# undefined by design (the corpus's own "Notifications", used
|
|
440
580
|
# deliberately to exercise the undelivered-reaction runtime path)
|
|
441
|
-
#
|
|
442
|
-
# `
|
|
443
|
-
#
|
|
444
|
-
# keyword invented to declare "this one's fine."
|
|
581
|
+
# declares so on its own policy — `expect_undelivered: true`,
|
|
582
|
+
# checked by `expected_undelivered_findings` — rather than being
|
|
583
|
+
# named in a core allowlist.
|
|
445
584
|
if known_domains && !known_domains.include?(target)
|
|
446
585
|
findings << Finding.new(kind: :unknown_target_domain, severity: :error, subject: policy.name,
|
|
447
586
|
message: "across #{target.inspect} names a domain nowhere in the corpus " \
|
|
448
|
-
"this check has booted — a typo, or a
|
|
449
|
-
"
|
|
450
|
-
"
|
|
451
|
-
"explained, not silently assumed correct")
|
|
587
|
+
"this check has booted — a typo, or a target intentionally never " \
|
|
588
|
+
"reached, which the policy itself declares with " \
|
|
589
|
+
"across #{target.inspect}, expect_undelivered: true")
|
|
452
590
|
end
|
|
453
591
|
|
|
454
592
|
findings
|
|
455
593
|
end
|
|
456
594
|
|
|
595
|
+
# A declared undelivered target, held to its declaration. The two
|
|
596
|
+
# findings an unreachable `across` target raises (unknown target,
|
|
597
|
+
# unacknowledged relationship) are what the policy declared it
|
|
598
|
+
# expects, so they are not raised. What is raised is the declaration
|
|
599
|
+
# going stale: the target is a domain this corpus actually booted,
|
|
600
|
+
# or the sibling hecksagon attaches or subscribes to it — either way
|
|
601
|
+
# the reaction can be delivered, and the declaration is now a lie.
|
|
602
|
+
# `known_domains` is nil for a single-target run, which can then only
|
|
603
|
+
# check the hecksagon half.
|
|
604
|
+
def expected_undelivered_findings(policy, hecksagon, known_domains)
|
|
605
|
+
target = policy.target_domain
|
|
606
|
+
reached = []
|
|
607
|
+
reached << "#{target} is a domain this corpus boots" if known_domains&.include?(target)
|
|
608
|
+
if hecksagon
|
|
609
|
+
reached << "this hecksagon uses_framework #{target.inspect}" if hecksagon.framework_members.include?(target)
|
|
610
|
+
if hecksagon.subscriptions.any? { |subscribed| Naming.qualifier(subscribed) == target }
|
|
611
|
+
reached << "this hecksagon subscribes to #{target}"
|
|
612
|
+
end
|
|
613
|
+
end
|
|
614
|
+
return [] if reached.empty?
|
|
615
|
+
|
|
616
|
+
[Finding.new(kind: :stale_undelivered_expectation, severity: :error, subject: policy.name,
|
|
617
|
+
message: "across #{target.inspect}, expect_undelivered: true — but #{reached.join(' and ')}, " \
|
|
618
|
+
"so the reaction can be delivered after all; drop expect_undelivered: or remove " \
|
|
619
|
+
"what reaches #{target}")]
|
|
620
|
+
end
|
|
621
|
+
|
|
457
622
|
# ── shared enumeration ────────────────────────────────────────────
|
|
458
623
|
|
|
459
|
-
# A
|
|
624
|
+
# A port operation emits too — the primary/driving port an adapter
|
|
460
625
|
# outside the bluebook calls through (see hecksagon_builder.rb) is a
|
|
461
626
|
# second, real source of events, alongside a command's own `emits`.
|
|
462
|
-
# Ports attach to the aggregate/bluebook from the
|
|
627
|
+
# Ports attach to the aggregate/bluebook from the sibling `.hecksagon`
|
|
463
628
|
# file, not this one — a caller that boots only the `.bluebook` (as
|
|
464
629
|
# the fixtures under spec/fixtures/model_check/ do, having no
|
|
465
630
|
# hecksagon at all) simply finds none, which is correct : nothing
|
|
466
631
|
# can be deaf to an event that isn't even wired up yet.
|
|
632
|
+
#
|
|
633
|
+
# An outbound operation (`asks`) emits through a different door — it
|
|
634
|
+
# declares no `.emits` at all (`PortOperationBuilder#refuse_wrong_
|
|
635
|
+
# words!` refuses one that tries), naming its two real endings
|
|
636
|
+
# `.answers`/`.refuses` instead (`PortOperation#initialize`). Reading
|
|
637
|
+
# only `.emits` left every `asks`'s own two events invisible to this
|
|
638
|
+
# method — real, live events a policy genuinely reacts to
|
|
639
|
+
# (`Clearance.SuitePassed`/`SuiteFailed`, `Ticket.IssueFiled`/
|
|
640
|
+
# `IssueFilingRefused`), reported as `deaf_policy` findings until this
|
|
641
|
+
# read both. `.compact` because an inbound operation's `.answers`/
|
|
642
|
+
# `.refuses` are always nil (there is no channel back to tell), which
|
|
643
|
+
# would otherwise seed every emitted-events set with a stray nil.
|
|
467
644
|
def emitted_events(bluebook)
|
|
468
645
|
aggregate_emits = bluebook.aggregates.flat_map do |aggregate|
|
|
469
646
|
aggregate.commands.map(&:emits) +
|
|
470
647
|
aggregate.entities.flat_map { |entity| entity.commands.map(&:emits) } +
|
|
471
|
-
aggregate.ports
|
|
648
|
+
port_operation_events(aggregate.ports)
|
|
472
649
|
end
|
|
473
|
-
chapter_emits = bluebook.ports
|
|
650
|
+
chapter_emits = port_operation_events(bluebook.ports)
|
|
651
|
+
|
|
652
|
+
(aggregate_emits + chapter_emits).flatten.compact.uniq
|
|
653
|
+
end
|
|
474
654
|
|
|
475
|
-
|
|
655
|
+
# One operation, either of its own sources of events — an inbound
|
|
656
|
+
# `tells` names its own via `.emits`; an outbound `asks` has none
|
|
657
|
+
# (`PortOperationBuilder#refuse_wrong_words!` refuses one that
|
|
658
|
+
# tries) and names its two real endings `.answers`/`.refuses`
|
|
659
|
+
# instead. Pulled out of `emitted_events` above purely to keep that
|
|
660
|
+
# method's own branching low enough to read at a glance — every
|
|
661
|
+
# port, aggregate-owned or chapter-level, asks this the same way.
|
|
662
|
+
def port_operation_events(ports)
|
|
663
|
+
ports.flat_map { |port| port.operations.flat_map { |op| [*op.emits, op.answers, op.refuses] } }
|
|
476
664
|
end
|
|
477
665
|
|
|
478
666
|
# Fully-qualified, the same spelling DispatchSpec#command_name
|
|
@@ -490,6 +678,35 @@ module Hecks
|
|
|
490
678
|
end
|
|
491
679
|
end
|
|
492
680
|
|
|
681
|
+
# An aggregate-owned port operation is a triggerable verb too —
|
|
682
|
+
# `ReactionInvocation#resolve_target`'s own port-operation branch
|
|
683
|
+
# resolves one by the exact same two-segment tail shape ("Aggregate::
|
|
684
|
+
# Port.Operation", the aggregate then the port then the operation,
|
|
685
|
+
# dot-joined past the domain) an entity command uses, checked first,
|
|
686
|
+
# same order `Dispatcher#dispatch` already resolves a live verb in.
|
|
687
|
+
# Only an aggregate's own ports (`aggregate.ports`) are in scope here
|
|
688
|
+
# — a policy's `trigger` always names one aggregate, never a chapter-
|
|
689
|
+
# level port with no owner to address through.
|
|
690
|
+
def port_verbs_of(bluebook)
|
|
691
|
+
bluebook.aggregates.flat_map do |aggregate|
|
|
692
|
+
aggregate.ports.flat_map do |port|
|
|
693
|
+
port.operations.map do |operation|
|
|
694
|
+
"#{bluebook.name}::#{aggregate.hecks_name}.#{port.name}.#{operation.hecks_name}"
|
|
695
|
+
end
|
|
696
|
+
end
|
|
697
|
+
end
|
|
698
|
+
end
|
|
699
|
+
|
|
700
|
+
# Every triggerable verb, as a triple — `verbs_of` (ordinary/entity
|
|
701
|
+
# commands) plus `port_verbs_of` (port operations), each parsed
|
|
702
|
+
# through `Naming.split_verb` so a caller never has to compare two
|
|
703
|
+
# spellings of the same verb as strings (see `policy_findings`'s own
|
|
704
|
+
# `unknown_trigger` check for why that comparison has to happen this
|
|
705
|
+
# way, not as `include?` on a raw string).
|
|
706
|
+
def triggerable_verbs(bluebook)
|
|
707
|
+
(verbs_of(bluebook) + port_verbs_of(bluebook)).to_set { |verb| Naming.split_verb(verb) }
|
|
708
|
+
end
|
|
709
|
+
|
|
493
710
|
def bare(event) = event.to_s.split("::").last
|
|
494
711
|
end
|
|
495
712
|
end
|