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
|
@@ -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,45 +31,42 @@ 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
|
-
[:unacknowledged_relationship, "FlagKeyReturn"],
|
|
66
|
-
[:unknown_target_domain, "FlagKeyReturn"]
|
|
67
|
-
]
|
|
68
|
-
# QualityControl WAS the first domain in this corpus to trigger an
|
|
69
|
-
# `asks`/`tells` PORT OPERATION from a `policy`, and used to carry
|
|
70
|
-
# two entries here for it — both GONE now, not just quieted:
|
|
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:
|
|
71
70
|
#
|
|
72
71
|
# `deaf_policy` (ClearOnPass, RefuseOnFail, RecordTheIssue,
|
|
73
72
|
# RecordTheRefusal) went first: `emitted_events` below now reads an
|
|
@@ -82,7 +81,7 @@ module Hecks
|
|
|
82
81
|
# never actually a `Naming`/`PolicyBuilder` defect, confirmed by
|
|
83
82
|
# tracing the real dispatch path rather than assuming the comment
|
|
84
83
|
# that used to sit here: `Naming.command_ref`'s bare-constant
|
|
85
|
-
# rewrite
|
|
84
|
+
# rewrite does leave `trigger Ticket::IssueTracker::File` (aggregate,
|
|
86
85
|
# port, operation) as "Ticket::IssueTracker.File", a leftover `::`
|
|
87
86
|
# past the aggregate — but `PolicyInterpreter#deliver` re-qualifies
|
|
88
87
|
# every trigger with this domain's own name before dispatch
|
|
@@ -92,7 +91,7 @@ module Hecks
|
|
|
92
91
|
# resolve_target`, PR #520, predating this entry's own removal) —
|
|
93
92
|
# confirmed live: a real dispatch through `Ticket.Submit` fires
|
|
94
93
|
# `IssueFiled`/`TicketFiled` exactly as declared. The actual gap was
|
|
95
|
-
# entirely in
|
|
94
|
+
# entirely in this checker: `verbs_of` never enumerated a port
|
|
96
95
|
# operation as a triggerable verb at all, and `policy_findings`
|
|
97
96
|
# compared raw strings instead of `Naming.split_verb` triples the
|
|
98
97
|
# way `handler_findings`'s own `unknown_dispatch` check already does
|
|
@@ -106,16 +105,20 @@ module Hecks
|
|
|
106
105
|
# `hecksagon:`/`known_domains:` — both optional, both `nil`-safe
|
|
107
106
|
# (every existing caller with no sibling hecksagon, or checking one
|
|
108
107
|
# domain in isolation, behaves exactly as before). `hecksagon` is
|
|
109
|
-
#
|
|
108
|
+
# this bluebook's own sibling wiring file, if the caller loaded one
|
|
110
109
|
# (see `emitted_events`'s own comment on why a caller that didn't
|
|
111
110
|
# simply finds none, correctly). `known_domains` is the caller's
|
|
112
|
-
#
|
|
111
|
+
# own corpus-wide view — every bluebook/hecksagon name it has
|
|
113
112
|
# booted anywhere, across every domain it has looked at, not just
|
|
114
113
|
# this one — used only to catch a typo'd `across`/`uses_framework`
|
|
115
114
|
# target; see `cross_domain_policy_findings`'s own comment for why
|
|
116
115
|
# this can only ever be a corpus-scoped heuristic, never a general
|
|
117
116
|
# correctness guarantee.
|
|
118
|
-
|
|
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)
|
|
119
122
|
findings = []
|
|
120
123
|
bluebook.aggregates.each do |aggregate|
|
|
121
124
|
findings.concat(lifecycle_findings(aggregate, aggregate))
|
|
@@ -123,10 +126,68 @@ module Hecks
|
|
|
123
126
|
end
|
|
124
127
|
bluebook.process_managers.each { |process_manager| findings.concat(saga_findings(bluebook, process_manager)) }
|
|
125
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))
|
|
126
132
|
findings
|
|
127
133
|
end
|
|
128
134
|
|
|
129
|
-
# ──
|
|
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) ──
|
|
130
191
|
|
|
131
192
|
def lifecycle_findings(aggregate, declaring)
|
|
132
193
|
lifecycle = declaring.lifecycle
|
|
@@ -198,7 +259,7 @@ module Hecks
|
|
|
198
259
|
).uniq
|
|
199
260
|
end
|
|
200
261
|
|
|
201
|
-
# Least fixpoint from the default state: an
|
|
262
|
+
# Least fixpoint from the default state: an unconstrained
|
|
202
263
|
# transition always fires, from wherever the machine is ; a
|
|
203
264
|
# constrained one fires once any of its named sources is reached.
|
|
204
265
|
def reachable_states(lifecycle)
|
|
@@ -222,8 +283,8 @@ module Hecks
|
|
|
222
283
|
reached
|
|
223
284
|
end
|
|
224
285
|
|
|
225
|
-
# A state with no
|
|
226
|
-
# 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
|
|
227
288
|
# unconstrained transition" — the default state of a lifecycle
|
|
228
289
|
# with only constrained transitions is legitimately allowed to sit
|
|
229
290
|
# forever, since nothing about *entering* it via default implies
|
|
@@ -231,7 +292,7 @@ module Hecks
|
|
|
231
292
|
# explicitly delivered somewhere.
|
|
232
293
|
#
|
|
233
294
|
# Scoped to `default` alone, and only when the lifecycle actually
|
|
234
|
-
# declares real transitions elsewhere: an
|
|
295
|
+
# declares real transitions elsewhere: an empty lifecycle (no
|
|
235
296
|
# transitions at all) doesn't get this exemption — that's not "a
|
|
236
297
|
# machine whose entry point deliberately awaits external action,"
|
|
237
298
|
# it's much more likely a lifecycle nobody finished wiring, and
|
|
@@ -249,7 +310,7 @@ module Hecks
|
|
|
249
310
|
|
|
250
311
|
def saga_findings(bluebook, process_manager)
|
|
251
312
|
emitted = emitted_events(bluebook)
|
|
252
|
-
# (domain, aggregate, command)
|
|
313
|
+
# (domain, aggregate, command) triples, not raw strings — see
|
|
253
314
|
# `handler_findings`'s own comment on the dispatch side for why:
|
|
254
315
|
# `Naming.split_verb` is what makes an entity verb's two legitimate
|
|
255
316
|
# spellings (`Naming.command_ref`'s own `::`-then-`.` rewrite vs.
|
|
@@ -285,11 +346,11 @@ module Hecks
|
|
|
285
346
|
end
|
|
286
347
|
end
|
|
287
348
|
|
|
288
|
-
#
|
|
349
|
+
# One handler's own findings — deaf_handler, unknown_dispatch (one
|
|
289
350
|
# per dispatch), and unarmed_compensation (one per compensating
|
|
290
351
|
# dispatch), pulled out of saga_findings' own handler loop; each
|
|
291
352
|
# check reads only this handler plus the domain-wide emitted/verbs
|
|
292
|
-
# sets saga_findings already resolved once, no state shared
|
|
353
|
+
# sets saga_findings already resolved once, no state shared between
|
|
293
354
|
# handlers.
|
|
294
355
|
def handler_findings(bluebook, process_manager, emitted, verbs, handler)
|
|
295
356
|
findings = []
|
|
@@ -305,7 +366,7 @@ module Hecks
|
|
|
305
366
|
end
|
|
306
367
|
|
|
307
368
|
handler.dispatches.each do |dispatch|
|
|
308
|
-
#
|
|
369
|
+
# **Always this domain** — same fix, same reason, as `SagaInterpreter
|
|
309
370
|
# #qualified` (BUG#6). This used to guess: a dispatch whose own
|
|
310
371
|
# `command_name` still carried a leftover `::` after `Naming.
|
|
311
372
|
# command_ref`'s own rewrite was read as "already qualified" and
|
|
@@ -320,7 +381,7 @@ module Hecks
|
|
|
320
381
|
# — instead of maintaining its own, independently-wrong copy of
|
|
321
382
|
# the same guess.
|
|
322
383
|
#
|
|
323
|
-
#
|
|
384
|
+
# Compared as a triple, not a string — `Naming.command_ref`'s
|
|
324
385
|
# own rewrite of an entity reference (`Manifest::Slot::Fill`)
|
|
325
386
|
# collapses to "Manifest::Slot.Fill" (`::` between aggregate and
|
|
326
387
|
# entity, `.` before the command); `verbs_of`'s own entity
|
|
@@ -345,7 +406,7 @@ module Hecks
|
|
|
345
406
|
"checker's scope, same as CommandRules#resolve_references")
|
|
346
407
|
end
|
|
347
408
|
|
|
348
|
-
# A `compensates`
|
|
409
|
+
# A `compensates` declared with nowhere to ever fire — the exact
|
|
349
410
|
# shape of the real bug this whole feature closes ("the
|
|
350
411
|
# reversal was written and never armed"), caught at build/
|
|
351
412
|
# model-check time instead of discovered in production. No
|
|
@@ -374,7 +435,7 @@ module Hecks
|
|
|
374
435
|
end
|
|
375
436
|
|
|
376
437
|
# A handler edge is only usable in the closure if it can actually
|
|
377
|
-
#
|
|
438
|
+
# fire — REFUSED always can (it is a compensation trigger, not an
|
|
378
439
|
# event), and any other handler needs its event genuinely emitted.
|
|
379
440
|
# Without this, a deaf handler's declared from_state -> to_state
|
|
380
441
|
# pair reads as connected even though nothing can ever traverse
|
|
@@ -408,8 +469,8 @@ module Hecks
|
|
|
408
469
|
emitted = emitted_events(bluebook)
|
|
409
470
|
findings = []
|
|
410
471
|
|
|
411
|
-
# `policy.event_name` (Naming.unqualified) —
|
|
412
|
-
# 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
|
|
413
474
|
# `on_event` carries a "." aggregate qualifier instead (PolicyBuilder
|
|
414
475
|
# stores whatever was typed, verbatim — see `on "Account.
|
|
415
476
|
# AccountFrozen"`), and `bare` left it untouched, silently comparing
|
|
@@ -429,13 +490,13 @@ module Hecks
|
|
|
429
490
|
# "#{domain}::#{trigger_command}" — the same join `verbs_of` builds
|
|
430
491
|
# independently, so the two spellings have to be compared as FQNs.
|
|
431
492
|
#
|
|
432
|
-
#
|
|
493
|
+
# Compared as a triple, not a string — `handler_findings`'s own
|
|
433
494
|
# `unknown_dispatch` check (BUG#6) already applies this fix for a
|
|
434
495
|
# saga's dispatch; a policy's `trigger` needed the identical one. A
|
|
435
|
-
# policy triggering an `asks`/`tells`
|
|
496
|
+
# policy triggering an `asks`/`tells` port operation (`Aggregate::
|
|
436
497
|
# Port::Operation`, three colon-joined segments — `Naming.command_ref`'s
|
|
437
498
|
# bare-constant rewrite turns this into "Aggregate::Port.Operation",
|
|
438
|
-
# a leftover `::` past the aggregate) is a real,
|
|
499
|
+
# a leftover `::` past the aggregate) is a real, working dispatch —
|
|
439
500
|
# `PolicyInterpreter#deliver` qualifies it with this domain's own
|
|
440
501
|
# name before `Naming.split_verb` ever sees it, and `split_verb`
|
|
441
502
|
# already folds that leftover `::` into the dot-joined tail
|
|
@@ -458,17 +519,17 @@ module Hecks
|
|
|
458
519
|
|
|
459
520
|
# ── cross-domain policies (Context Mapping) ───────────────────────
|
|
460
521
|
#
|
|
461
|
-
# `uses_framework "X"` already
|
|
462
|
-
# merges X's own bluebook into
|
|
463
|
-
# 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
|
|
464
525
|
# relationship — it dispatches into X over real cross-Lambda RPC in
|
|
465
526
|
# the Rust host (`rust/host/src/lambda_client.rs`). Neither is a new
|
|
466
|
-
# word; this makes the
|
|
527
|
+
# word; this makes the choice between them checked instead of a
|
|
467
528
|
# prose comment nobody enforces (`examples/banking/bluebook/
|
|
468
529
|
# banking.hecksagon`'s own hand-written note explaining why
|
|
469
530
|
# Compliance is reached via `across`, never `uses_framework`).
|
|
470
531
|
#
|
|
471
|
-
#
|
|
532
|
+
# No new keyword anywhere — ADR 0025 principle 1 ("one idea, one
|
|
472
533
|
# spelling") refuses a `relationship:`/`as:` argument that would
|
|
473
534
|
# just restate, as a string, the fact the chosen keyword (
|
|
474
535
|
# `uses_framework` vs `across`) already states completely. The
|
|
@@ -476,14 +537,15 @@ module Hecks
|
|
|
476
537
|
# the finding's own name and this comment, and in prose docs — not
|
|
477
538
|
# in the grammar.
|
|
478
539
|
def cross_domain_policy_findings(policy, hecksagon, known_domains)
|
|
540
|
+
return expected_undelivered_findings(policy, hecksagon, known_domains) if policy.expect_undelivered
|
|
479
541
|
return [] unless hecksagon # no sibling hecksagon loaded — nothing to check a relationship against.
|
|
480
542
|
|
|
481
543
|
target = policy.target_domain
|
|
482
544
|
findings = []
|
|
483
545
|
|
|
484
546
|
if hecksagon.framework_members.include?(target)
|
|
485
|
-
#
|
|
486
|
-
#
|
|
547
|
+
# Shared kernel and customer/supplier are mutually exclusive
|
|
548
|
+
# claims about the same target — `uses_framework` means "X is
|
|
487
549
|
# loaded in-process, right here"; `across` means "X is a
|
|
488
550
|
# separate deployment, reached only by RPC." Declaring both is
|
|
489
551
|
# either a pointless RPC to a domain already local, or a
|
|
@@ -495,7 +557,7 @@ module Hecks
|
|
|
495
557
|
"relationship declarations contradict each other for the same " \
|
|
496
558
|
"target domain")
|
|
497
559
|
elsif hecksagon.subscriptions.none? { |subscribed| Naming.qualifier(subscribed) == target }
|
|
498
|
-
#
|
|
560
|
+
# This is what finally gives `subscribe` real teeth — checked
|
|
499
561
|
# here, at model-check time, still never routed at runtime
|
|
500
562
|
# (nothing dispatches off a `subscribe` line; see hecksagon.md's
|
|
501
563
|
# own "checked, not routed" section). ADR 0025 names `subscribe`
|
|
@@ -508,43 +570,67 @@ module Hecks
|
|
|
508
570
|
"attach it in-process instead")
|
|
509
571
|
end
|
|
510
572
|
|
|
511
|
-
#
|
|
512
|
-
# ever be a
|
|
573
|
+
# **Typo detection, deliberately weaker** — `known_domains` can only
|
|
574
|
+
# ever be a monorepo-scoped heuristic: a real external hecks
|
|
513
575
|
# consumer's own domain (this repo's own embryonaut/lifeadelics-
|
|
514
576
|
# shaped case) lives in a genuinely separate repository this
|
|
515
577
|
# corpus scan can never see, so a target this check cannot find
|
|
516
|
-
# is "unknown to THIS corpus," never proof of a typo.
|
|
517
|
-
#
|
|
518
|
-
# 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
|
|
519
580
|
# deliberately to exercise the undelivered-reaction runtime path)
|
|
520
|
-
#
|
|
521
|
-
# `
|
|
522
|
-
#
|
|
523
|
-
# 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.
|
|
524
584
|
if known_domains && !known_domains.include?(target)
|
|
525
585
|
findings << Finding.new(kind: :unknown_target_domain, severity: :error, subject: policy.name,
|
|
526
586
|
message: "across #{target.inspect} names a domain nowhere in the corpus " \
|
|
527
|
-
"this check has booted — a typo, or a
|
|
528
|
-
"
|
|
529
|
-
"
|
|
530
|
-
"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")
|
|
531
590
|
end
|
|
532
591
|
|
|
533
592
|
findings
|
|
534
593
|
end
|
|
535
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
|
+
|
|
536
622
|
# ── shared enumeration ────────────────────────────────────────────
|
|
537
623
|
|
|
538
|
-
# A
|
|
624
|
+
# A port operation emits too — the primary/driving port an adapter
|
|
539
625
|
# outside the bluebook calls through (see hecksagon_builder.rb) is a
|
|
540
626
|
# second, real source of events, alongside a command's own `emits`.
|
|
541
|
-
# Ports attach to the aggregate/bluebook from the
|
|
627
|
+
# Ports attach to the aggregate/bluebook from the sibling `.hecksagon`
|
|
542
628
|
# file, not this one — a caller that boots only the `.bluebook` (as
|
|
543
629
|
# the fixtures under spec/fixtures/model_check/ do, having no
|
|
544
630
|
# hecksagon at all) simply finds none, which is correct : nothing
|
|
545
631
|
# can be deaf to an event that isn't even wired up yet.
|
|
546
632
|
#
|
|
547
|
-
#
|
|
633
|
+
# An outbound operation (`asks`) emits through a different door — it
|
|
548
634
|
# declares no `.emits` at all (`PortOperationBuilder#refuse_wrong_
|
|
549
635
|
# words!` refuses one that tries), naming its two real endings
|
|
550
636
|
# `.answers`/`.refuses` instead (`PortOperation#initialize`). Reading
|
|
@@ -552,7 +638,7 @@ module Hecks
|
|
|
552
638
|
# method — real, live events a policy genuinely reacts to
|
|
553
639
|
# (`Clearance.SuitePassed`/`SuiteFailed`, `Ticket.IssueFiled`/
|
|
554
640
|
# `IssueFilingRefused`), reported as `deaf_policy` findings until this
|
|
555
|
-
# read both. `.compact` because an
|
|
641
|
+
# read both. `.compact` because an inbound operation's `.answers`/
|
|
556
642
|
# `.refuses` are always nil (there is no channel back to tell), which
|
|
557
643
|
# would otherwise seed every emitted-events set with a stray nil.
|
|
558
644
|
def emitted_events(bluebook)
|
|
@@ -566,7 +652,7 @@ module Hecks
|
|
|
566
652
|
(aggregate_emits + chapter_emits).flatten.compact.uniq
|
|
567
653
|
end
|
|
568
654
|
|
|
569
|
-
#
|
|
655
|
+
# One operation, either of its own sources of events — an inbound
|
|
570
656
|
# `tells` names its own via `.emits`; an outbound `asks` has none
|
|
571
657
|
# (`PortOperationBuilder#refuse_wrong_words!` refuses one that
|
|
572
658
|
# tries) and names its two real endings `.answers`/`.refuses`
|
|
@@ -592,13 +678,13 @@ module Hecks
|
|
|
592
678
|
end
|
|
593
679
|
end
|
|
594
680
|
|
|
595
|
-
#
|
|
681
|
+
# An aggregate-owned port operation is a triggerable verb too —
|
|
596
682
|
# `ReactionInvocation#resolve_target`'s own port-operation branch
|
|
597
683
|
# resolves one by the exact same two-segment tail shape ("Aggregate::
|
|
598
684
|
# Port.Operation", the aggregate then the port then the operation,
|
|
599
685
|
# dot-joined past the domain) an entity command uses, checked first,
|
|
600
686
|
# same order `Dispatcher#dispatch` already resolves a live verb in.
|
|
601
|
-
# Only an aggregate's
|
|
687
|
+
# Only an aggregate's own ports (`aggregate.ports`) are in scope here
|
|
602
688
|
# — a policy's `trigger` always names one aggregate, never a chapter-
|
|
603
689
|
# level port with no owner to address through.
|
|
604
690
|
def port_verbs_of(bluebook)
|
|
@@ -611,7 +697,7 @@ module Hecks
|
|
|
611
697
|
end
|
|
612
698
|
end
|
|
613
699
|
|
|
614
|
-
#
|
|
700
|
+
# Every triggerable verb, as a triple — `verbs_of` (ordinary/entity
|
|
615
701
|
# commands) plus `port_verbs_of` (port operations), each parsed
|
|
616
702
|
# through `Naming.split_verb` so a caller never has to compare two
|
|
617
703
|
# spellings of the same verb as strings (see `policy_findings`'s own
|
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
module Hecks
|
|
2
2
|
module Bluebook
|
|
3
|
-
#
|
|
3
|
+
# **Which regexes a bluebook may say**.
|
|
4
4
|
#
|
|
5
5
|
# A `pattern:` is a fact about a value, carried in a bluebook — declared
|
|
6
6
|
# data, not Ruby code, so it must not lean on what any one engine happens
|
|
7
7
|
# to accept. Regex engines disagree in two different ways :
|
|
8
8
|
#
|
|
9
|
-
#
|
|
9
|
+
# only a backtracking engine can match it — lookahead, lookbehind,
|
|
10
10
|
# backreferences, atomic groups, possessive quantifiers. None of these
|
|
11
11
|
# can be matched in linear time, and linear-time engines refuse them
|
|
12
12
|
# outright. Refused here for the same reason.
|
|
13
13
|
#
|
|
14
|
-
#
|
|
14
|
+
# every engine parses it and they mean different things — the dangerous
|
|
15
15
|
# half, because nothing errors. `\d` `\w` `\s` are ASCII in some engines
|
|
16
16
|
# and Unicode in others ; `[:digit:]` and friends flip the same way in
|
|
17
17
|
# the other direction. Both families are refused, and a domain spells
|
|
18
18
|
# the range it means.
|
|
19
19
|
#
|
|
20
20
|
# What remains — explicit ranges, alternation, quantifiers, anchors,
|
|
21
|
-
# groups — reads identically everywhere, with `^` and `$` as
|
|
21
|
+
# groups — reads identically everywhere, with `^` and `$` as line anchors
|
|
22
22
|
# (Ruby's reading). The evidence is spec/corpus/fixtures/patterns.json.
|
|
23
23
|
module PatternSubset
|
|
24
24
|
Rejection = Struct.new(:construct, :reason)
|
|
@@ -57,13 +57,13 @@ module Hecks
|
|
|
57
57
|
|
|
58
58
|
# nil when the pattern is admitted, a Rejection when it is not.
|
|
59
59
|
#
|
|
60
|
-
# A
|
|
60
|
+
# A character walk, deliberately plain : the subset is defined by this
|
|
61
61
|
# walk, and a cleverer spelling would hide what it admits. An escaped
|
|
62
|
-
# construct is a
|
|
62
|
+
# construct is a literal, not a violation — `\(\?=` is the three
|
|
63
63
|
# characters "(?=" and says nothing about lookahead — which is why this
|
|
64
64
|
# steps over each backslash pair rather than matching the pattern as a
|
|
65
65
|
# whole.
|
|
66
|
-
# One character walk is the subset's
|
|
66
|
+
# One character walk is the subset's definition (see the method
|
|
67
67
|
# comment above): each construct-check is a branch in a single
|
|
68
68
|
# ordered pass sharing `index`/`in_class`/`class_start`. Splitting
|
|
69
69
|
# the branches into separate methods would force those three cursor
|
|
@@ -75,7 +75,7 @@ module Hecks
|
|
|
75
75
|
def validate(pattern)
|
|
76
76
|
chars = pattern.to_s.chars
|
|
77
77
|
index = 0
|
|
78
|
-
# A
|
|
78
|
+
# A character-class interior is a different alphabet : inside `[...]`,
|
|
79
79
|
# `*`, `+`, `?`, `(`, `?` are literal characters, not quantifiers or
|
|
80
80
|
# group syntax — `[*+]` means "a literal asterisk or plus". `]` is
|
|
81
81
|
# only the class's close when it isn't the first character after `[`
|
|
@@ -132,7 +132,7 @@ module Hecks
|
|
|
132
132
|
nil
|
|
133
133
|
end
|
|
134
134
|
|
|
135
|
-
#
|
|
135
|
+
# Spelled out, not derived from the key : these strings are the refusal
|
|
136
136
|
# a caller reads.
|
|
137
137
|
CONSTRUCTS = {
|
|
138
138
|
backreference: "backreference",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
#
|
|
2
|
-
#
|
|
1
|
+
# Generated — projected from the language's own Policy aggregate.
|
|
2
|
+
# Do not edit: the holding half is rendered, and Behaviour::Policy
|
|
3
3
|
# is where anything hand-written belongs.
|
|
4
4
|
require_relative "behaviour/policy"
|
|
5
5
|
|
|
@@ -10,28 +10,30 @@ module Hecks
|
|
|
10
10
|
include Behaviour::Policy
|
|
11
11
|
|
|
12
12
|
emits_ir(
|
|
13
|
-
name:
|
|
14
|
-
on_event:
|
|
15
|
-
trigger_command:
|
|
16
|
-
target_domain:
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
13
|
+
name: :name,
|
|
14
|
+
on_event: :on_event,
|
|
15
|
+
trigger_command: :trigger_command,
|
|
16
|
+
target_domain: :target_domain,
|
|
17
|
+
expect_undelivered: :expect_undelivered,
|
|
18
|
+
where: :where,
|
|
19
|
+
for_each: :for_each,
|
|
20
|
+
with_spec: -> { with_spec.map { |key, value| [key.to_s, Bluebook.render_value(value)] } },
|
|
21
|
+
where_ast: -> { where_ast }
|
|
21
22
|
)
|
|
22
23
|
|
|
23
|
-
attr_reader :name, :on_event, :trigger_command, :target_domain, :where, :for_each, :with_spec
|
|
24
|
+
attr_reader :name, :on_event, :trigger_command, :target_domain, :expect_undelivered, :where, :for_each, :with_spec
|
|
24
25
|
|
|
25
|
-
#
|
|
26
|
+
# Aggregate, declared and deliberately off the wire
|
|
26
27
|
# the wire format is a pinned contract, and it does not carry
|
|
27
28
|
# where a policy was written before the builder hoisted it
|
|
28
29
|
attr_accessor :aggregate
|
|
29
30
|
|
|
30
|
-
def initialize(name:, on_event: nil, trigger_command: nil, target_domain: nil, where: nil, for_each: nil, with_spec: [], aggregate: nil)
|
|
31
|
+
def initialize(name:, on_event: nil, trigger_command: nil, target_domain: nil, expect_undelivered: false, where: nil, for_each: nil, with_spec: [], aggregate: nil)
|
|
31
32
|
@name = name.to_s
|
|
32
33
|
@on_event = on_event
|
|
33
34
|
@trigger_command = trigger_command
|
|
34
35
|
@target_domain = target_domain
|
|
36
|
+
@expect_undelivered = expect_undelivered.to_s == "true"
|
|
35
37
|
@where = where
|
|
36
38
|
@for_each = for_each
|
|
37
39
|
@with_spec = with_spec
|