hecks 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/hecks/adapters/driven/claude_code.rb +7 -7
- data/lib/hecks/adapters/driven/d1.rb +187 -23
- data/lib/hecks/adapters/driven/folder.rb +10 -10
- data/lib/hecks/adapters/driven/google_authentication.rb +8 -8
- data/lib/hecks/adapters/driven/governance_authorization.rb +31 -13
- data/lib/hecks/adapters/driven/heki/journal.rb +60 -2
- data/lib/hecks/adapters/driven/heki/saga_store.rb +5 -5
- data/lib/hecks/adapters/driven/heki.rb +13 -7
- data/lib/hecks/adapters/driven/identity_registry.rb +2 -2
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +3 -3
- data/lib/hecks/adapters/driven/lambda/client.rb +34 -9
- data/lib/hecks/adapters/driven/lambda.rb +39 -33
- data/lib/hecks/adapters/driven/local_storage.rb +17 -10
- data/lib/hecks/adapters/driven/memory.rb +205 -9
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +1 -1
- data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
- data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
- data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
- data/lib/hecks/adapters/driven/postgres.rb +175 -28
- data/lib/hecks/adapters/driven/postgres_era.adapter +5 -0
- data/lib/hecks/adapters/driven/prism.rb +4 -4
- data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
- data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
- data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
- data/lib/hecks/adapters/driven/sqlite.rb +181 -21
- data/lib/hecks/adapters/driven.rb +4 -4
- data/lib/hecks/adapters/driving/github_webhook.rb +145 -0
- data/lib/hecks/behaviors/dsl.rb +2 -2
- data/lib/hecks/behaviors/expectations.rb +51 -23
- data/lib/hecks/behaviors/ir.rb +1 -1
- data/lib/hecks/behaviors/rspec.rb +1 -1
- data/lib/hecks/behaviors/runner.rb +2 -2
- data/lib/hecks/behaviors.rb +1 -1
- data/lib/hecks/bluebook/aggregate.rb +13 -13
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +10 -10
- data/lib/hecks/bluebook/assembly/build.rb +1 -1
- data/lib/hecks/bluebook/assembly/contract.rb +39 -16
- data/lib/hecks/bluebook/assembly/contracts.rb +40 -38
- data/lib/hecks/bluebook/assembly/marks.rb +18 -18
- data/lib/hecks/bluebook/assembly/specializer.rb +19 -21
- data/lib/hecks/bluebook/assembly.rb +9 -9
- data/lib/hecks/bluebook/attribute.rb +9 -9
- data/lib/hecks/bluebook/behaviour/aggregate.rb +11 -11
- data/lib/hecks/bluebook/behaviour/attribute.rb +5 -5
- data/lib/hecks/bluebook/behaviour/chapter.rb +23 -5
- data/lib/hecks/bluebook/behaviour/command.rb +23 -23
- data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -3
- data/lib/hecks/bluebook/behaviour/entity.rb +8 -8
- data/lib/hecks/bluebook/behaviour/hexagon.rb +4 -4
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +5 -5
- data/lib/hecks/bluebook/behaviour/policy.rb +12 -12
- data/lib/hecks/bluebook/behaviour/process_manager.rb +7 -7
- data/lib/hecks/bluebook/behaviour/query.rb +1 -1
- data/lib/hecks/bluebook/behaviour/read_model.rb +8 -8
- data/lib/hecks/bluebook/behaviour/traits.rb +12 -12
- data/lib/hecks/bluebook/behaviour/value_object.rb +6 -6
- data/lib/hecks/bluebook/capabilities.rb +27 -0
- data/lib/hecks/bluebook/chapter.rb +28 -9
- data/lib/hecks/bluebook/command.rb +12 -12
- data/lib/hecks/bluebook/domain_port.rb +9 -9
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +96 -96
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +41 -41
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +22 -2
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +111 -74
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +48 -30
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +103 -103
- data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
- data/lib/hecks/bluebook/dsl/entity_builder.rb +56 -56
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +89 -30
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +17 -17
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
- data/lib/hecks/bluebook/dsl/policy_builder.rb +30 -21
- data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +35 -35
- data/lib/hecks/bluebook/dsl/query_builder.rb +5 -5
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +34 -34
- data/lib/hecks/bluebook/dsl/rule_reference.rb +41 -39
- data/lib/hecks/bluebook/dsl/translation_builder.rb +9 -9
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +16 -16
- data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
- data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
- data/lib/hecks/bluebook/entity.rb +11 -11
- data/lib/hecks/bluebook/expression/ast_json.rb +20 -20
- data/lib/hecks/bluebook/expression/ast_reader.rb +3 -3
- data/lib/hecks/bluebook/expression/canonical_form.rb +9 -9
- data/lib/hecks/bluebook/expression/evaluator.rb +18 -18
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +18 -18
- data/lib/hecks/bluebook/expression/resolver.rb +60 -62
- data/lib/hecks/bluebook/hexagon.rb +1 -1
- data/lib/hecks/bluebook/lifecycle.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/judge.rb +122 -86
- data/lib/hecks/bluebook/meta_validator/plan.rb +39 -39
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -2
- data/lib/hecks/bluebook/meta_validator/readings.rb +43 -43
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +44 -37
- data/lib/hecks/bluebook/meta_validator/shapes.rb +25 -21
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +145 -31
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +6 -6
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +5 -5
- data/lib/hecks/bluebook/meta_validator.rb +70 -70
- data/lib/hecks/bluebook/model_check.rb +301 -84
- data/lib/hecks/bluebook/pattern_subset.rb +9 -9
- data/lib/hecks/bluebook/policy.rb +15 -13
- data/lib/hecks/bluebook/process_manager.rb +14 -14
- data/lib/hecks/bluebook/project_register.rb +6 -6
- data/lib/hecks/bluebook/query.rb +4 -4
- data/lib/hecks/bluebook/read_model.rb +14 -14
- data/lib/hecks/bluebook/reference.rb +8 -8
- data/lib/hecks/bluebook/smoke_test.rb +19 -19
- data/lib/hecks/bluebook/synthesizer.rb +12 -12
- data/lib/hecks/bluebook/translation.rb +4 -4
- data/lib/hecks/bluebook/value_object.rb +6 -6
- data/lib/hecks/bluebook.rb +2 -2
- data/lib/hecks/codemod/legacy_dispatch_args.rb +299 -0
- data/lib/hecks/codemod/legacy_dispatch_recorder.rb +186 -0
- data/lib/hecks/codemod.rb +36 -35
- data/lib/hecks/construct.rb +6 -6
- data/lib/hecks/corpus.rb +317 -0
- data/lib/hecks/deprecation.rb +95 -0
- data/lib/hecks/doc/reference.rb +19 -19
- data/lib/hecks/embryonaut_bluebook.rb +11 -11
- data/lib/hecks/facade/cli_door.rb +69 -10
- data/lib/hecks/facade/cli_runner.rb +105 -24
- data/lib/hecks/facade/command_request.rb +23 -0
- data/lib/hecks/facade/handle.rb +79 -32
- data/lib/hecks/facade/json_door.rb +106 -25
- data/lib/hecks/facade/surface/aggregate_door.rb +42 -27
- data/lib/hecks/facade/surface/chapter.rb +26 -17
- data/lib/hecks/facade/surface.rb +16 -3
- data/lib/hecks/facade.rb +15 -4
- data/lib/hecks/forms/app.rb +46 -30
- data/lib/hecks/forms/command_form_renderer.rb +70 -9
- data/lib/hecks/forms/field_renderer.rb +142 -6
- data/lib/hecks/forms/field_shape.rb +183 -20
- data/lib/hecks/forms/html.rb +51 -7
- data/lib/hecks/forms/index_renderer.rb +14 -2
- data/lib/hecks/forms/params.rb +120 -23
- data/lib/hecks/forms/port_argument.rb +2 -2
- data/lib/hecks/forms/query_form_renderer.rb +2 -2
- data/lib/hecks/forms/record_renderer.rb +2 -2
- data/lib/hecks/forms/record_table.rb +1 -1
- data/lib/hecks/forms/value_object_shape.rb +3 -3
- data/lib/hecks/forms.rb +24 -4
- data/lib/hecks/fqn.rb +1 -1
- data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
- data/lib/hecks/framework.rb +48 -17
- data/lib/hecks/freezer.rb +11 -11
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +90 -90
- data/lib/hecks/fuzzing/combination_miner.rb +119 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +361 -0
- data/lib/hecks/fuzzing/coverage_campaign.rb +118 -0
- data/lib/hecks/fuzzing/differential.rb +158 -0
- data/lib/hecks/fuzzing/domain_generator.rb +694 -0
- data/lib/hecks/fuzzing/era_boundary.rb +124 -0
- data/lib/hecks/fuzzing/form_census.rb +199 -0
- data/lib/hecks/fuzzing/generated_domain_check.rb +95 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +6 -6
- data/lib/hecks/fuzzing/isolated_boot.rb +226 -38
- data/lib/hecks/fuzzing/nondeterministic.rb +67 -0
- data/lib/hecks/fuzzing/persistence_parity.rb +161 -0
- data/lib/hecks/fuzzing/properties/corrections.rb +100 -0
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +321 -41
- data/lib/hecks/fuzzing/properties/guards.rb +129 -26
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +32 -32
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +31 -11
- data/lib/hecks/fuzzing/properties/outbox.rb +142 -0
- data/lib/hecks/fuzzing/properties/querying.rb +17 -17
- data/lib/hecks/fuzzing/properties.rb +84 -39
- data/lib/hecks/fuzzing/qa_settings.rb +152 -0
- data/lib/hecks/fuzzing/replay.rb +224 -114
- data/lib/hecks/fuzzing/rotation_priority.rb +94 -0
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +113 -0
- data/lib/hecks/fuzzing/self_consistency.rb +676 -0
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +526 -0
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +101 -30
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +55 -8
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +31 -11
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +123 -32
- data/lib/hecks/fuzzing/sequence_generator.rb +152 -31
- data/lib/hecks/fuzzing/shrinker.rb +197 -0
- data/lib/hecks/fuzzing/structural_skips.rb +39 -0
- data/lib/hecks/fuzzing/sweep_depth.rb +53 -0
- data/lib/hecks/fuzzing/target_capabilities.rb +180 -0
- data/lib/hecks/fuzzing/value_generator.rb +66 -14
- data/lib/hecks/fuzzing.rb +11 -0
- data/lib/hecks/grammar/evolve.rb +10 -10
- data/lib/hecks/grammar.rb +7 -7
- data/lib/hecks/ir.rb +13 -13
- data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
- data/lib/hecks/language/bluebook/policy.bluebook +11 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +365 -15
- data/lib/hecks/language/oidc.json +5 -0
- data/lib/hecks/literal.rb +9 -9
- data/lib/hecks/naming.rb +89 -21
- data/lib/hecks/ports/access_control.rb +58 -2
- data/lib/hecks/ports/agent/answers.rb +83 -6
- data/lib/hecks/ports/agent.rb +119 -35
- data/lib/hecks/ports/authentication.rb +44 -4
- data/lib/hecks/ports/authorization.rb +53 -11
- data/lib/hecks/ports/clock.rb +42 -23
- data/lib/hecks/ports/extraction.rb +16 -0
- data/lib/hecks/ports/identity_assignment.rb +24 -2
- data/lib/hecks/ports/identity_generation.rb +17 -3
- data/lib/hecks/ports/identity_resolution.rb +18 -1
- data/lib/hecks/ports/loading.rb +4 -0
- data/lib/hecks/ports/persistence/append_only.rb +172 -8
- data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
- data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
- data/lib/hecks/ports/persistence/execution.rb +4 -0
- data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
- data/lib/hecks/ports/persistence/plugin.rb +42 -4
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
- data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +160 -58
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +28 -5
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +44 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +57 -4
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +338 -83
- data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +42 -8
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -19
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +5 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
- data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
- data/lib/hecks/ports/persistence/repository_factory.rb +29 -6
- data/lib/hecks/ports/persistence/state_codec.rb +319 -0
- data/lib/hecks/ports/persistence.rb +36 -1
- data/lib/hecks/ports/projection.rb +61 -7
- data/lib/hecks/ports/query/in_memory.rb +3 -3
- data/lib/hecks/ports/query/ordering.rb +6 -6
- data/lib/hecks/ports/query.rb +35 -0
- data/lib/hecks/projections/bootstrap_table.rb +112 -0
- data/lib/hecks/projections/diagrams.rb +75 -75
- data/lib/hecks/projections/glossary/html.rb +250 -0
- data/lib/hecks/projections/glossary/markdown.rb +105 -0
- data/lib/hecks/projections/glossary/mermaid.rb +110 -0
- data/lib/hecks/projections/glossary/page.css +271 -0
- data/lib/hecks/projections/glossary/page.js +72 -0
- data/lib/hecks/projections/glossary/sections.rb +17 -0
- data/lib/hecks/projections/glossary/sentences.rb +205 -0
- data/lib/hecks/projections/glossary.rb +214 -286
- data/lib/hecks/projections/ir.rb +1 -1
- data/lib/hecks/projections/model/deviations.rb +18 -17
- data/lib/hecks/projections/model.rb +25 -21
- data/lib/hecks/projections/oidc.rb +7 -7
- data/lib/hecks/projections/parser_table.rb +5 -5
- data/lib/hecks/projections/reference.rb +3 -3
- data/lib/hecks/projections/rust_vocabulary.rb +443 -0
- data/lib/hecks/projections/shape.rb +2 -2
- data/lib/hecks/projections/statements.rb +11 -11
- data/lib/hecks/projections/vocabulary.rb +9 -9
- data/lib/hecks/projections.rb +5 -3
- data/lib/hecks/projector/cli_projector.rb +29 -29
- data/lib/hecks/projector/docs_projector.rb +13 -13
- data/lib/hecks/projector/exporter.rb +42 -21
- data/lib/hecks/projector/ir_projector.rb +1 -1
- data/lib/hecks/projector/narrate_projector.rb +15 -22
- data/lib/hecks/projector/target.rb +13 -13
- data/lib/hecks/projector.rb +15 -15
- data/lib/hecks/query_ir.rb +47 -47
- data/lib/hecks/query_specification/common/comparators.rb +19 -3
- data/lib/hecks/query_specification/common/comparison.rb +132 -24
- data/lib/hecks/query_specification/common/dsl.rb +65 -9
- data/lib/hecks/query_specification/common/null_policy.rb +57 -13
- data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
- data/lib/hecks/query_specification/common/options.rb +25 -0
- data/lib/hecks/query_specification/field_path.rb +69 -15
- data/lib/hecks/query_specification/hop_path.rb +57 -20
- data/lib/hecks/query_specification/read_model/specification.rb +4 -0
- data/lib/hecks/rendering.rb +3 -3
- data/lib/hecks/router/namespace_installer.rb +3 -3
- data/lib/hecks/router.rb +1 -1
- data/lib/hecks/runtime/aggregate_lock.rb +11 -11
- data/lib/hecks/runtime/boot_gates.rb +3 -3
- data/lib/hecks/runtime/caller.rb +8 -8
- data/lib/hecks/runtime/capability_graph.rb +2 -2
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +22 -25
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +64 -67
- data/lib/hecks/runtime/command_interpreter.rb +159 -102
- data/lib/hecks/runtime/command_rules/admissibility.rb +69 -69
- data/lib/hecks/runtime/command_rules/arithmetic.rb +137 -91
- data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
- data/lib/hecks/runtime/command_rules/emission.rb +18 -1
- data/lib/hecks/runtime/command_rules/references.rb +94 -19
- data/lib/hecks/runtime/command_rules.rb +3 -0
- data/lib/hecks/runtime/dependency_planning.rb +11 -11
- data/lib/hecks/runtime/dispatcher.rb +283 -90
- data/lib/hecks/runtime/entity_element.rb +282 -46
- data/lib/hecks/runtime/entity_interpreter.rb +160 -69
- data/lib/hecks/runtime/errors.rb +19 -19
- data/lib/hecks/runtime/event.rb +6 -6
- data/lib/hecks/runtime/identity.rb +22 -22
- data/lib/hecks/runtime/instance.rb +39 -14
- data/lib/hecks/runtime/interpreting.rb +12 -12
- data/lib/hecks/runtime/invocation.rb +276 -0
- data/lib/hecks/runtime/loader.rb +14 -14
- data/lib/hecks/runtime/outbox.rb +23 -23
- data/lib/hecks/runtime/policy_interpreter.rb +54 -54
- data/lib/hecks/runtime/port_operation_interpreter.rb +22 -19
- data/lib/hecks/runtime/query_interpreter.rb +111 -56
- data/lib/hecks/runtime/reaction_invocation.rb +76 -9
- data/lib/hecks/runtime/read_model_interpreter.rb +40 -40
- data/lib/hecks/runtime/rebuild_sweep.rb +4 -4
- data/lib/hecks/runtime/reference_hop.rb +6 -6
- data/lib/hecks/runtime/refusal_wording.rb +92 -112
- data/lib/hecks/runtime/registry/saga_persistence.rb +21 -21
- data/lib/hecks/runtime/registry/verification.rb +36 -26
- data/lib/hecks/runtime/registry.rb +56 -27
- data/lib/hecks/runtime/remote_dispatcher.rb +38 -23
- data/lib/hecks/runtime/routing.rb +10 -88
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +17 -17
- data/lib/hecks/runtime/saga_interpreter.rb +93 -56
- data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
- data/lib/hecks/runtime/tenant_check.rb +9 -9
- data/lib/hecks/runtime/tenant_scope.rb +5 -5
- data/lib/hecks/runtime/value/admission.rb +75 -30
- data/lib/hecks/runtime/value/coercion.rb +379 -226
- data/lib/hecks/runtime/value/entity_list_coercion.rb +248 -0
- data/lib/hecks/runtime/value.rb +28 -23
- data/lib/hecks/runtime.rb +7 -7
- data/lib/hecks/storehouse.rb +64 -64
- data/lib/hecks/version.rb +3 -3
- data/lib/hecks/vocabulary.rb +207 -5
- data/lib/hecks.rb +13 -11
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +11 -11
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +14 -12
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +11 -11
- metadata +43 -2
data/lib/hecks/projector.rb
CHANGED
|
@@ -13,28 +13,28 @@ module Hecks
|
|
|
13
13
|
# working, golden-tested example of the shape every future projector
|
|
14
14
|
# (`:rust`, `:ul`, `:openid`, ...) is meant to follow.
|
|
15
15
|
#
|
|
16
|
-
# The unit is
|
|
16
|
+
# The unit is one bluebook's IR, not a whole booted registry — matching
|
|
17
17
|
# every real projection target (Rust/UL/OIDC all project one domain at
|
|
18
18
|
# a time), and deliberately narrower than `Exporter.call`'s own
|
|
19
19
|
# multi-domain shape.
|
|
20
|
-
#
|
|
20
|
+
# Three kinds of "PROJECT", told apart by what they need as input.
|
|
21
21
|
# Only the first belongs in this registry.
|
|
22
22
|
#
|
|
23
|
-
#
|
|
24
|
-
# that
|
|
23
|
+
# a projection takes a chapter's declaration and answers something
|
|
24
|
+
# that describes the domain: its IR, its storage shape, an OIDC scope
|
|
25
25
|
# manifest, the parser's keyword table, the reference pages. Inert,
|
|
26
26
|
# derived, and runnable against any chapter that carries what it
|
|
27
27
|
# declares it needs. These are what `register` holds.
|
|
28
28
|
#
|
|
29
|
-
#
|
|
30
|
-
# something that
|
|
29
|
+
# an export takes a declaration and its bindings and answers
|
|
30
|
+
# something that is the domain, running elsewhere — rust/project.rb's
|
|
31
31
|
# generated crate, the WASM artifact, the SAM template
|
|
32
32
|
# bin/project_deploy renders. It needs the `.world`/`.hecksagon` a
|
|
33
33
|
# projection never looks at, because a running system has to know how
|
|
34
34
|
# it is wired. That is the whole reason bin/project_deploy cannot use
|
|
35
35
|
# this protocol: `call(bluebook:, options:)` has no channel for it.
|
|
36
36
|
#
|
|
37
|
-
#
|
|
37
|
+
# a state projection takes records — a domain after dispatch — and is
|
|
38
38
|
# a read-model question wearing the same word.
|
|
39
39
|
# `bin/expression_projection` is the one of these: its operators are
|
|
40
40
|
# not declared anywhere, they are what exists after
|
|
@@ -42,7 +42,7 @@ module Hecks
|
|
|
42
42
|
# dispatches. Converting it into this registry would be a category
|
|
43
43
|
# error, however much its name suggests otherwise.
|
|
44
44
|
#
|
|
45
|
-
#
|
|
45
|
+
# **One word, three other meanings** — worth naming too, because grepping
|
|
46
46
|
# "projection" turns all of these up and none is the above:
|
|
47
47
|
#
|
|
48
48
|
# Ports::Projection read-model catch-up, events folded into state
|
|
@@ -68,7 +68,7 @@ module Hecks
|
|
|
68
68
|
# `bluebook:` is kept as the keyword because it is the shipped
|
|
69
69
|
# spelling and every existing caller uses it — but what it accepts is
|
|
70
70
|
# any construct that emits IR, and `admits!` is what decides whether
|
|
71
|
-
#
|
|
71
|
+
# this target can actually take the one handed over.
|
|
72
72
|
def call(name, bluebook:, options: {})
|
|
73
73
|
projector = registry.fetch(name.to_sym) do
|
|
74
74
|
raise UnknownProjector, "no projector registered for #{name.inspect} — registered: #{registered.sort.inspect}"
|
|
@@ -77,12 +77,12 @@ module Hecks
|
|
|
77
77
|
projector.call(bluebook: bluebook, options: options)
|
|
78
78
|
end
|
|
79
79
|
|
|
80
|
-
# A projection names the
|
|
80
|
+
# A projection names the capabilities it needs; this refuses a
|
|
81
81
|
# construct that lacks one, before the projector runs.
|
|
82
82
|
#
|
|
83
|
-
#
|
|
83
|
+
# One check covers both shapes. An ordinary construct includes its
|
|
84
84
|
# capabilities and a class-shaped one — Command, Entity, ValueObject
|
|
85
|
-
# —
|
|
85
|
+
# — extends them, and `is_a?` consults the singleton chain, so it
|
|
86
86
|
# answers for an extended module as readily as an included one. This
|
|
87
87
|
# started as two checks on the assumption it would not; a spec
|
|
88
88
|
# asserting the assumption failed, which is the only reason the
|
|
@@ -107,7 +107,7 @@ module Hecks
|
|
|
107
107
|
|
|
108
108
|
def capable?(construct, capability) = construct.is_a?(capability)
|
|
109
109
|
|
|
110
|
-
# What
|
|
110
|
+
# What kind of artifact a registered target emits — asked of the
|
|
111
111
|
# projection rather than inferred from what it returned.
|
|
112
112
|
def emits_for(name)
|
|
113
113
|
projector = registry.fetch(name.to_sym) { return :artifact }
|
|
@@ -132,7 +132,7 @@ module Hecks
|
|
|
132
132
|
target
|
|
133
133
|
end
|
|
134
134
|
|
|
135
|
-
#
|
|
135
|
+
# Writing is the caller's choice, not the projector's. A projector
|
|
136
136
|
# returns an artifact and never touches disk, which is what lets
|
|
137
137
|
# spec/projector_spec.rb compare `:ir`'s output against a golden
|
|
138
138
|
# fixture without a tmpdir. `out:` is the only thing that writes.
|
|
@@ -178,7 +178,7 @@ Hecks::Projector.register(:docs, Hecks::Projector::DocsProjector)
|
|
|
178
178
|
Hecks::Projector.register(:narrate, Hecks::Projector::NarrateProjector)
|
|
179
179
|
Hecks::Projector.register(:cli, Hecks::Projector::CliProjector)
|
|
180
180
|
|
|
181
|
-
# The
|
|
181
|
+
# The targets are required from lib/hecks.rb, immediately after this
|
|
182
182
|
# file — deliberately not from here. A target requires this file (it
|
|
183
183
|
# needs `Target` and the registry), so requiring them back from here
|
|
184
184
|
# would close a genuine `circular require considered harmful` loop. Ruby
|
data/lib/hecks/query_ir.rb
CHANGED
|
@@ -4,7 +4,7 @@ require_relative "projections/model"
|
|
|
4
4
|
require_relative "fuzzing/properties"
|
|
5
5
|
|
|
6
6
|
module Hecks
|
|
7
|
-
#
|
|
7
|
+
# The shared core behind `bin/query_ir` (a text CLI) and
|
|
8
8
|
# `bin/hecks_query_ir_mcp` (an MCP server exposing the same two
|
|
9
9
|
# queries as tools) — one implementation, two front ends, the same
|
|
10
10
|
# reason `Hecks::Codemod` exists once rather than per-script.
|
|
@@ -14,7 +14,7 @@ module Hecks
|
|
|
14
14
|
Codemod = Hecks::Codemod
|
|
15
15
|
Deviations = Hecks::Projections::Model::Deviations
|
|
16
16
|
|
|
17
|
-
#
|
|
17
|
+
# The same mapping spec/model_shape_conformance_spec.rb's own
|
|
18
18
|
# MODEL_CONSTRUCTS holds — kept here rather than shared from the
|
|
19
19
|
# spec (a spec file is not a library other code should require),
|
|
20
20
|
# matching Deviations' own doc comment: "the generator and the gate
|
|
@@ -44,7 +44,7 @@ module Hecks
|
|
|
44
44
|
|
|
45
45
|
# The real structural diff between what a Ruby IR class emits
|
|
46
46
|
# (Class.ir_spec.keys) and what the self-hosted meta-domain declares
|
|
47
|
-
# for it — the
|
|
47
|
+
# for it — the same comparison spec/model_shape_conformance_spec.rb
|
|
48
48
|
# makes, reusing its own Deviations data so this can never silently
|
|
49
49
|
# drift from what that gate actually checks.
|
|
50
50
|
def construct_diff(name)
|
|
@@ -54,8 +54,8 @@ module Hecks
|
|
|
54
54
|
declared = meta_declared(name)
|
|
55
55
|
emitted = klass.ir_spec.keys
|
|
56
56
|
|
|
57
|
-
accounted = declared.reject { |field| Deviations
|
|
58
|
-
Deviations
|
|
57
|
+
accounted = declared.reject { |field| Deviations.parent_ref?(field) } -
|
|
58
|
+
Deviations.judge_only(name) -
|
|
59
59
|
Deviations.folded(name).values.flatten -
|
|
60
60
|
Deviations.off_the_wire(name) -
|
|
61
61
|
Deviations.dynamic_tail(name) -
|
|
@@ -77,22 +77,22 @@ module Hecks
|
|
|
77
77
|
targets.map { |name| construct_diff(name) }
|
|
78
78
|
end
|
|
79
79
|
|
|
80
|
-
# Every given/ensures/invariant
|
|
80
|
+
# Every given/ensures/invariant declaration reachable from a booted
|
|
81
81
|
# registry, walked recursively — every owner's own `.preconditions`/
|
|
82
82
|
# `.invariants` (block-declared rules), every value object's own
|
|
83
83
|
# `.invariants`, and every command's own `.givens`/`.ensures`
|
|
84
|
-
# (`.givens` too, not just `.ensures` — a command's own
|
|
84
|
+
# (`.givens` too, not just `.ensures` — a command's own local
|
|
85
85
|
# `given("x") { block }` not yet hoisted to its owner, round 4's own
|
|
86
86
|
# starting shape, would otherwise be invisible).
|
|
87
87
|
#
|
|
88
|
-
#
|
|
88
|
+
# Not keyed by object identity (a real, hard-won correction — see
|
|
89
89
|
# the comment on `duplicates`' own dedup below for why: a bare
|
|
90
90
|
# `given("x")` reference and its owner's own block declaration are
|
|
91
|
-
# the
|
|
92
|
-
# (S14 — every bluebook is judged by dispatching its
|
|
91
|
+
# the same Ruby object at DSL build time, but `MetaValidator.call`
|
|
92
|
+
# (S14 — every bluebook is judged by dispatching its own IR into the
|
|
93
93
|
# self-hosted grammar, then reconstructed via `Assembly.call` from
|
|
94
94
|
# flat rows) rebuilds the whole graph fresh from there. By the time
|
|
95
|
-
# any caller reads `chapter.aggregates`,
|
|
95
|
+
# any caller reads `chapter.aggregates`, every given/invariant is
|
|
96
96
|
# already a distinct object, whether it was block-declared or
|
|
97
97
|
# bare-referenced — object identity carries no signal past that
|
|
98
98
|
# point, for any construct, not just this one).
|
|
@@ -114,7 +114,7 @@ module Hecks
|
|
|
114
114
|
rules
|
|
115
115
|
end
|
|
116
116
|
|
|
117
|
-
#
|
|
117
|
+
# The recursive walk `collect_rules` drives — pulled out of that method
|
|
118
118
|
# (pure extraction, identical traversal and Rule shapes) as its own
|
|
119
119
|
# named, self-recursive method rather than a lambda closing over the
|
|
120
120
|
# same locals. `rules` is the one piece of state every call shares —
|
|
@@ -147,14 +147,14 @@ module Hecks
|
|
|
147
147
|
end
|
|
148
148
|
private_class_method :walk_construct_rules
|
|
149
149
|
|
|
150
|
-
# A rule's
|
|
150
|
+
# A rule's owner — the construct path a "(declared)" location names
|
|
151
151
|
# directly, or (for a command-level `.givens`/`.ensures` entry) the
|
|
152
152
|
# path with its trailing `.CommandName` segment stripped. Two rules
|
|
153
|
-
# sharing an owner are the
|
|
153
|
+
# sharing an owner are the same declaration read twice (an owner's
|
|
154
154
|
# own precondition, and a command under it referencing that
|
|
155
155
|
# precondition by name) — not two independent ones.
|
|
156
156
|
#
|
|
157
|
-
#
|
|
157
|
+
# Public, not a `duplicates`-only internal — `bin/codemod_hoist_
|
|
158
158
|
# local_givens` reads it directly to group `collect_rules`' own
|
|
159
159
|
# output by owner itself, the same reading `duplicates`' own
|
|
160
160
|
# `declaration_count` makes.
|
|
@@ -167,23 +167,23 @@ module Hecks
|
|
|
167
167
|
# Grouped by (kind, description, canonical), not canonical text
|
|
168
168
|
# alone — a generic one-liner like `!value.to_s.empty?` legitimately
|
|
169
169
|
# recurs dozens of times for unrelated fields; the real signal is
|
|
170
|
-
# the
|
|
170
|
+
# the same rule (same description, same predicate), which is also
|
|
171
171
|
# exactly what the given/invariant reference mechanism itself
|
|
172
172
|
# resolves on.
|
|
173
173
|
#
|
|
174
|
-
#
|
|
174
|
+
# Deduped by owner, not object identity (`collect_rules`' own
|
|
175
175
|
# comment has the full story — identity is gone by the time this
|
|
176
176
|
# reads the registry). Within a group, every command-level rule
|
|
177
|
-
# whose
|
|
177
|
+
# whose owner already has its own "(declared)" entry in the same
|
|
178
178
|
# group is just that declaration read again through a reference —
|
|
179
179
|
# `Account.Open`/`Account.Credit`/etc. all naming `Account`'s own
|
|
180
|
-
# `given("customer is active")` count as Account's
|
|
180
|
+
# `given("customer is active")` count as Account's one declaration,
|
|
181
181
|
# not nine. A command-level rule with no matching owner declaration
|
|
182
|
-
# (a
|
|
182
|
+
# (a local, not-yet-hoisted `given("x") { block }`) counts as its
|
|
183
183
|
# own standalone declaration — two different commands independently
|
|
184
|
-
# writing the identical local predicate
|
|
184
|
+
# writing the identical local predicate is two declarations, a real
|
|
185
185
|
# hoisting opportunity. A group is reported only when it adds up to
|
|
186
|
-
#
|
|
186
|
+
# more than one real declaration this way.
|
|
187
187
|
#
|
|
188
188
|
# `domains: []` means "the self-hosted meta-domain only" — pass real
|
|
189
189
|
# domain directories explicitly to include them, or `nil` (the
|
|
@@ -210,48 +210,48 @@ module Hecks
|
|
|
210
210
|
end
|
|
211
211
|
end
|
|
212
212
|
|
|
213
|
-
# `given`
|
|
213
|
+
# `given` only (not `invariant`/`ensures`) also covers cross-entity
|
|
214
214
|
# coverage — one piece's own entity-level declaration, shared with
|
|
215
|
-
# any
|
|
215
|
+
# any other piece nested under the same root aggregate (real corpus
|
|
216
216
|
# this closes: SafeDepositBox's own `Visit`/`KeyIssuance`, two
|
|
217
217
|
# different pieces on one head). A rule owned by a nested entity
|
|
218
218
|
# (its own owner path has more than one segment) is covered when
|
|
219
|
-
#
|
|
220
|
-
# aggregate — not just under its
|
|
219
|
+
# some "(declared)" entry exists anywhere under that same root
|
|
220
|
+
# aggregate — not just under its own exact owner — matching the
|
|
221
221
|
# DSL's own pool, threaded unchanged through an aggregate's whole
|
|
222
222
|
# entity tree (`AggregateBuilder#entity`'s own comment).
|
|
223
223
|
#
|
|
224
|
-
# A
|
|
225
|
-
# close:
|
|
224
|
+
# A known, accepted gap this does not (and structurally cannot)
|
|
225
|
+
# close: chapter-wide given sharing (`AggregateBuilder#given`'s own
|
|
226
226
|
# bare form, `docs/implemented/resolution-rules/chapter-given.md`) — `Account`,
|
|
227
227
|
# `SafeDepositBox`, and `OnboardingCase` each still show as their
|
|
228
|
-
# own "(declared)" owner here even
|
|
228
|
+
# own "(declared)" owner here even after `SafeDepositBox`/
|
|
229
229
|
# `OnboardingCase` were converted to bare chapter-wide references,
|
|
230
|
-
# because a
|
|
231
|
-
# aggregate's `@named_givens` — the
|
|
230
|
+
# because a referenced given still write-throughs into its own
|
|
231
|
+
# aggregate's `@named_givens` — the same reason `collect_rules`'
|
|
232
232
|
# own top comment already gives for why object identity carries no
|
|
233
|
-
# signal past a bluebook's own build: the
|
|
233
|
+
# signal past a bluebook's own build: the exported IR cannot tell
|
|
234
234
|
# "I declared this myself" apart from "I referenced someone else's
|
|
235
235
|
# declaration," because by the time anything reads `chapter.
|
|
236
236
|
# aggregates`, both look identical. Closing this would mean reading
|
|
237
|
-
#
|
|
237
|
+
# source text (bare `given(desc)` vs. block `given(desc) { ... }`),
|
|
238
238
|
# not the built IR this query is deliberately built on — a
|
|
239
239
|
# different, source-level tool, not a fix to this one. Treat a
|
|
240
240
|
# still-flagged group naming multiple aggregates as "verify by
|
|
241
|
-
# hand whether this is
|
|
241
|
+
# hand whether this is already a chapter-wide reference before
|
|
242
242
|
# assuming it's fresh duplication," not as an automatic signal
|
|
243
243
|
# either way.
|
|
244
244
|
#
|
|
245
|
-
#
|
|
245
|
+
# The identical gap, one level down: chapter-wide entity-scoped
|
|
246
246
|
# sharing (`EntityBuilder#given`'s own bare form,
|
|
247
247
|
# `docs/implemented/resolution-rules/chapter-entity-given.md`) hits this same wall for
|
|
248
248
|
# the same structural reason — `SafeDepositBox.Visit` still shows
|
|
249
249
|
# as its own "(declared)" owner here even after becoming a bare
|
|
250
250
|
# reference to `Account.LedgerEntry`'s declaration, because a piece
|
|
251
251
|
# resolving a chapter-wide reference still write-throughs the
|
|
252
|
-
# resolved `Given` into its own `@named_givens` (so
|
|
252
|
+
# resolved `Given` into its own `@named_givens` (so its own
|
|
253
253
|
# commands can read it back locally without a second hop). This is
|
|
254
|
-
# not a
|
|
254
|
+
# not a new limitation this feature introduces — it is the exact
|
|
255
255
|
# same IR-cannot-distinguish-declared-from-referenced fact, one
|
|
256
256
|
# scope wider. `bin/query_ir duplicates` confirms this directly:
|
|
257
257
|
# `Account.LedgerEntry (declared)` and `SafeDepositBox.Visit
|
|
@@ -278,10 +278,10 @@ module Hecks
|
|
|
278
278
|
end
|
|
279
279
|
private_class_method :declaration_count
|
|
280
280
|
|
|
281
|
-
#
|
|
281
|
+
# Shared text formatting — both `bin/query_ir` (a text CLI) and
|
|
282
282
|
# `bin/hecks_query_ir_mcp` (an MCP tool result, itself a text
|
|
283
283
|
# block) want the identical human-readable rendering; only the
|
|
284
|
-
#
|
|
284
|
+
# outer framing differs (plain stdout vs. a JSON-RPC content array).
|
|
285
285
|
def format_constructs(diffs)
|
|
286
286
|
diffs.map do |diff|
|
|
287
287
|
lines = ["== #{diff[:name]} =="]
|
|
@@ -303,26 +303,26 @@ module Hecks
|
|
|
303
303
|
end.join("\n\n")
|
|
304
304
|
end
|
|
305
305
|
|
|
306
|
-
#
|
|
307
|
-
# two `MetaValidator::Reconstruction` methods
|
|
306
|
+
# **One hand-typed construct-name per reconstruction method** — the only
|
|
307
|
+
# two `MetaValidator::Reconstruction` methods not driven generically
|
|
308
308
|
# through `Assembly::Contracts`' own table (its own header explains
|
|
309
309
|
# why: `aggregate(row)`/`entity(row)` predate the table and were
|
|
310
310
|
# never migrated). `impact_preview`'s own touchpoint 4 is checked
|
|
311
|
-
#
|
|
311
|
+
# only for these two — every other construct is read generically, so
|
|
312
312
|
# asking "does Command's own reconstruction method mention this
|
|
313
313
|
# field" is a question with no method to check.
|
|
314
314
|
RECONSTRUCTION_METHODS = { "Aggregate" => :aggregate, "Entity" => :entity }.freeze
|
|
315
315
|
|
|
316
|
-
#
|
|
316
|
+
# The six touchpoints `.claude/skills/bluebook-construct-creator/
|
|
317
317
|
# SKILL.md` walks in prose, checked structurally instead of by hand
|
|
318
|
-
# — for a construct/field pair
|
|
318
|
+
# — for a construct/field pair not yet fully propagated (typically
|
|
319
319
|
# mid-round, deciding what's left), or as a sanity check before the
|
|
320
320
|
# final gate sweep of a round already believed done. Every check
|
|
321
|
-
# here is
|
|
321
|
+
# here is best-effort and advisory, not a gate: a `false` does not
|
|
322
322
|
# always mean "not yet done" (a field can be legitimately exempt —
|
|
323
323
|
# `Deviations`' own named categories, `GUARANTEED_BY_CONSTRUCTION`,
|
|
324
324
|
# or `META_DOMAIN_KNOWN_GAPS`, the last of which lives in
|
|
325
|
-
# spec/fuzzing/meta_domain_coverage_spec.rb, a
|
|
325
|
+
# spec/fuzzing/meta_domain_coverage_spec.rb, a spec file this
|
|
326
326
|
# module deliberately never requires — see `CONSTRUCTS`' own
|
|
327
327
|
# comment on the same principle). Read the touchpoint's own
|
|
328
328
|
# existing gate (`model_shape_conformance_spec.rb`,
|
|
@@ -365,7 +365,7 @@ module Hecks
|
|
|
365
365
|
# the method's own body ends at the next line indented no deeper
|
|
366
366
|
# than its own `def` — the same boundary Ruby itself uses, read back
|
|
367
367
|
# textually because there is no live AST here, only a file to grep a
|
|
368
|
-
# slice of. `nil` (not `false`) for every
|
|
368
|
+
# slice of. `nil` (not `false`) for every other construct — this
|
|
369
369
|
# touchpoint genuinely does not apply to them (`RECONSTRUCTION_
|
|
370
370
|
# METHODS` only names the two hand-typed methods), and collapsing
|
|
371
371
|
# "does not apply" into "not done" would misreport a construct that
|
|
@@ -7,10 +7,16 @@ module Hecks
|
|
|
7
7
|
# vocabulary — and .render_value, the wire-rendering entry point
|
|
8
8
|
# shared by every literal-bearing spec struct.
|
|
9
9
|
module QuerySpecification
|
|
10
|
+
# The vocabulary every query-shaped construct shares — the clause structs
|
|
11
|
+
# (`WhereClause`, `OrderBy`, `LimitSpec`...), the `DSL` mixin that parses
|
|
12
|
+
# them, and the comparison and null rules every engine answers by. It is
|
|
13
|
+
# its own namespace so a plain `Bluebook::Query` and a
|
|
14
|
+
# `ReadModel::Specification` say one thing the same way rather than each
|
|
15
|
+
# carrying a copy.
|
|
10
16
|
module Common
|
|
11
17
|
# `none_in_state`, vendored addition not (yet) upstream hecks
|
|
12
|
-
# (migration plan task 4): a
|
|
13
|
-
# `where ref: { none_in_state: "Claim:held" }` holds true when
|
|
18
|
+
# (migration plan task 4): a cross-aggregate anti-join comparator --
|
|
19
|
+
# `where ref: { none_in_state: "Claim:held" }` holds true when no
|
|
14
20
|
# record in the named aggregate, keyed by this record's own field
|
|
15
21
|
# value, is currently in the named state. plan.bluebook's own
|
|
16
22
|
# description: "a keyed point lookup (HashMap hit), never a scan" --
|
|
@@ -20,10 +26,20 @@ module Hecks
|
|
|
20
26
|
COMPARATORS = Hecks::Vocabulary.symbols("QueryComparator")
|
|
21
27
|
end
|
|
22
28
|
|
|
29
|
+
# Renders a literal captured in a query clause as its self-describing
|
|
30
|
+
# wire spelling, for a spec struct's `to_h`.
|
|
31
|
+
#
|
|
23
32
|
# The specification structs' own name for the one wire spelling — see
|
|
24
|
-
# Hecks::Literal
|
|
33
|
+
# `Hecks::Literal`, which every other `to_h`-bound literal field
|
|
25
34
|
# shares. Kept as a word here because the structs below read better
|
|
26
35
|
# saying what they are doing than naming the module that does it.
|
|
36
|
+
#
|
|
37
|
+
# @param value [nil, Symbol, String, StateRef, Boolean, Integer, Float, Hash, Array] the
|
|
38
|
+
# literal as the bluebook author wrote it; Hash values and Array elements are
|
|
39
|
+
# rendered recursively
|
|
40
|
+
# @return [String] the wire text: a Symbol keeps its colon, a String its double quotes,
|
|
41
|
+
# and a number, a boolean and `nil` are bare
|
|
42
|
+
# @raise [ArgumentError] if `value`, or a value nested in it, is of any other class
|
|
27
43
|
def self.render_value(value) = Literal.render(value)
|
|
28
44
|
end
|
|
29
45
|
end
|
|
@@ -4,7 +4,7 @@ require_relative "../../runtime/value"
|
|
|
4
4
|
module Hecks
|
|
5
5
|
module QuerySpecification
|
|
6
6
|
module Common
|
|
7
|
-
#
|
|
7
|
+
# **The one comparator table**. There were two, near-identical copies of
|
|
8
8
|
# this — `Ports::Query::InMemory` (the path a Memory- or Heki-backed
|
|
9
9
|
# aggregate query actually runs) and `Runtime::QueryInterpreter`
|
|
10
10
|
# (entity/sub-list queries, and any adapter implementing no `:query`)
|
|
@@ -15,23 +15,25 @@ module Hecks
|
|
|
15
15
|
# real bugs twice already, both recorded in the comments those files
|
|
16
16
|
# carried. `none_in_state` was added to one copy only, so an ordinary
|
|
17
17
|
# Memory query's `none_in_state` clause fell to the `else` branch and
|
|
18
|
-
# silently excluded every row. And `comparable`
|
|
19
|
-
# anyone noticing: one copy took the
|
|
20
|
-
# object, the other only unwrapped when there was
|
|
18
|
+
# silently excluded every row. And `comparable` diverged without
|
|
19
|
+
# anyone noticing: one copy took the first numeric member of a value
|
|
20
|
+
# object, the other only unwrapped when there was exactly one — so a
|
|
21
21
|
# value object with two numeric members compared as a number on one
|
|
22
22
|
# path and as a whole Hash on the other.
|
|
23
23
|
#
|
|
24
|
-
# The callers still differ in how they
|
|
24
|
+
# The callers still differ in how they reach a value — one takes a
|
|
25
25
|
# registry argument, the other closes over `@registry`; one digs the
|
|
26
26
|
# field through `FieldPath`, the other is handed it — so they keep
|
|
27
27
|
# their own resolution and share only the comparison itself.
|
|
28
28
|
module Comparison
|
|
29
29
|
module_function
|
|
30
30
|
|
|
31
|
+
# Unwraps a value object to the one scalar a comparison can mean.
|
|
32
|
+
#
|
|
31
33
|
# A value object compared as a scalar, when which scalar is meant
|
|
32
34
|
# is not in doubt. Exactly one numeric member is unambiguous; a
|
|
33
35
|
# single-member value object is unambiguous whatever its type.
|
|
34
|
-
# Anything else is returned
|
|
36
|
+
# Anything else is returned unchanged rather than guessed at — two
|
|
35
37
|
# numeric members give no reason to prefer either, and picking the
|
|
36
38
|
# first silently compares a field the author never named.
|
|
37
39
|
#
|
|
@@ -40,6 +42,11 @@ module Hecks
|
|
|
40
42
|
# so this branch is a backstop rather than the primary guard: which
|
|
41
43
|
# member is meant is knowable at declaration time, and a refusal
|
|
42
44
|
# naming the candidates is worth more than any runtime reading.
|
|
45
|
+
#
|
|
46
|
+
# @param value [Runtime::Value, Hash, Object, nil] a held or wanted value; a
|
|
47
|
+
# `Runtime::Value` is read through its `to_h`
|
|
48
|
+
# @return [Object, nil] the sole numeric member, or the sole member, of a Hash-shaped
|
|
49
|
+
# value; otherwise `value` unchanged (a `Runtime::Value` comes back as its Hash)
|
|
43
50
|
def comparable(value)
|
|
44
51
|
value = value.to_h if value.is_a?(Runtime::Value)
|
|
45
52
|
return value unless value.is_a?(Hash)
|
|
@@ -51,9 +58,15 @@ module Hecks
|
|
|
51
58
|
value
|
|
52
59
|
end
|
|
53
60
|
|
|
54
|
-
#
|
|
61
|
+
# Lists the members a value object offers a scalar comparison, for a
|
|
55
62
|
# refusal that can name them. Empty when the value object is
|
|
56
63
|
# unambiguous — nothing to report.
|
|
64
|
+
#
|
|
65
|
+
# @param value_object [Class<Bluebook::ValueObject>] the declared shape (a subclass
|
|
66
|
+
# minted by `Bluebook::ValueObject.declare`, closed sets included) a query field names
|
|
67
|
+
# @return [Array<Symbol>] every attribute name when no single member can be meant;
|
|
68
|
+
# `[]` when there is exactly one attribute, or exactly one typed `Integer`,
|
|
69
|
+
# `Float` or `Numeric`
|
|
57
70
|
def ambiguous_members(value_object)
|
|
58
71
|
numerics = value_object.attributes.select { |a| NUMERIC_TYPES.include?(a.type.to_s) }
|
|
59
72
|
return [] if numerics.size == 1 || value_object.attributes.size == 1
|
|
@@ -63,15 +76,32 @@ module Hecks
|
|
|
63
76
|
|
|
64
77
|
NUMERIC_TYPES = %w[Integer Float Numeric].freeze
|
|
65
78
|
|
|
66
|
-
#
|
|
79
|
+
# Decides whether one where-clause comparison holds between the value
|
|
80
|
+
# a record holds and the value the query wants.
|
|
81
|
+
#
|
|
82
|
+
# A `case` over a closed, declared set (`Vocabulary::QueryComparator`,
|
|
67
83
|
# held equal to this list by spec/vocabulary_table_spec — see the
|
|
68
|
-
# `else` branch's own comment) is the whole point of
|
|
69
|
-
#
|
|
84
|
+
# `else` branch's own comment) is the whole point of the one
|
|
85
|
+
# comparator table this file's header describes: one place naming
|
|
70
86
|
# every comparator, not one method per comparator scattered across
|
|
71
87
|
# a module.
|
|
88
|
+
#
|
|
89
|
+
# @param operation [Symbol, String] the comparator name: `eq`, `ne`, `lt`, `lte`,
|
|
90
|
+
# `gt`, `gte`, `in`, `contains` or `none_in_state`
|
|
91
|
+
# @param held [Object, nil] the record's own value for the field; `nil` against a
|
|
92
|
+
# non-nil `want` satisfies no comparator except `none_in_state` (see
|
|
93
|
+
# `NullPolicy.unmatchable?`)
|
|
94
|
+
# @param want [Object, nil] the value compared against; for `in` an Array or a
|
|
95
|
+
# comma-separated String, for `none_in_state` an `"Aggregate:state"` String
|
|
96
|
+
# @param registry [Runtime::Registry, nil] used only by `none_in_state` to look the
|
|
97
|
+
# target aggregate up; `nil` makes that comparator hold
|
|
98
|
+
# @return [Boolean] whether the comparison holds; an ordered comparator over a
|
|
99
|
+
# non-`Numeric` operand is `false`
|
|
100
|
+
# @raise [Runtime::WiringError] if `operation` names no comparator in this table, or
|
|
101
|
+
# `none_in_state`'s target aggregate has no repository that can be wired
|
|
72
102
|
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
73
103
|
def holds?(operation, held, want, registry: nil)
|
|
74
|
-
# A NULL
|
|
104
|
+
# A NULL satisfies no comparison — NullPolicy.unmatchable? owns
|
|
75
105
|
# the rule and the reasoning, including why `none_in_state` is
|
|
76
106
|
# exempt from it.
|
|
77
107
|
return false if NullPolicy.unmatchable?(operation, held, want)
|
|
@@ -100,28 +130,51 @@ module Hecks
|
|
|
100
130
|
end
|
|
101
131
|
end
|
|
102
132
|
|
|
133
|
+
# Checks that both operands of an ordered comparison are numbers.
|
|
134
|
+
#
|
|
103
135
|
# gt/gte/lt/lte are numeric-only and silently false otherwise — a
|
|
104
136
|
# where-clause never raises the way a given does, and that contract
|
|
105
137
|
# predates the extraction (lt was already exactly this permissive).
|
|
138
|
+
#
|
|
139
|
+
# @param held [Object, nil] the record's own value for the field
|
|
140
|
+
# @param want [Object, nil] the value compared against
|
|
141
|
+
# @return [Boolean] `true` only when both are `Numeric`
|
|
106
142
|
def ordered?(held, want) = held.is_a?(Numeric) && want.is_a?(Numeric)
|
|
107
143
|
|
|
144
|
+
# Reads a comparator's list operand as the Strings membership is tested against.
|
|
145
|
+
#
|
|
108
146
|
# `in` reads a comma-separated list — a real Array survives
|
|
109
147
|
# untouched (a bluebook's own in-process value, before any wire
|
|
110
148
|
# serialisation), each element unwrapped the same way a scalar
|
|
111
|
-
# field is. This is `in`'s reading of
|
|
149
|
+
# field is. This is `in`'s reading of its argument (a caller may
|
|
112
150
|
# legitimately pass "a,b,c" meaning "any of these") — unrelated to
|
|
113
|
-
# `contains`, which reads the
|
|
151
|
+
# `contains`, which reads the stored field. See `contains?`.
|
|
152
|
+
#
|
|
153
|
+
# @param value [Array, String, Object, nil] an Array of elements, or anything whose
|
|
154
|
+
# `to_s` is a comma-separated list such as `"a, b,c"`
|
|
155
|
+
# @return [Array<String>] one String per member: Array elements unwrapped through
|
|
156
|
+
# `comparable` then `to_s`; split parts stripped of surrounding whitespace; `[]`
|
|
157
|
+
# for `nil` or an empty String
|
|
114
158
|
def members(value)
|
|
115
159
|
return value.map { |element| comparable(element).to_s } if value.is_a?(Array)
|
|
116
160
|
|
|
117
161
|
value.to_s.split(",").map(&:strip)
|
|
118
162
|
end
|
|
119
163
|
|
|
164
|
+
# Answers `in`: whether the held value, or any element of a held Array,
|
|
165
|
+
# occurs in the wanted list.
|
|
166
|
+
#
|
|
120
167
|
# A folded reference hop asks whether the locally-held identity is
|
|
121
168
|
# among the matching target identities. A has_many relationship holds
|
|
122
169
|
# several identities, so the same question becomes an intersection:
|
|
123
|
-
# does
|
|
170
|
+
# does any held identity occur in the wanted set? Scalar `in` retains
|
|
124
171
|
# its existing one-candidate behavior.
|
|
172
|
+
#
|
|
173
|
+
# @param held [Array, Object] the record's own value; an Array contributes each
|
|
174
|
+
# element as a candidate, anything else is the single candidate
|
|
175
|
+
# @param want [Array, String, Object] the wanted set, read through `members`
|
|
176
|
+
# @return [Boolean] whether any candidate, unwrapped through `comparable` and
|
|
177
|
+
# compared as a String, is a wanted member
|
|
125
178
|
def any_member_in?(held, want)
|
|
126
179
|
wanted = members(want)
|
|
127
180
|
candidates = held.is_a?(Array) ? held : [held]
|
|
@@ -129,24 +182,35 @@ module Hecks
|
|
|
129
182
|
candidates.any? { |candidate| wanted.include?(comparable(candidate).to_s) }
|
|
130
183
|
end
|
|
131
184
|
|
|
132
|
-
# `contains
|
|
133
|
-
#
|
|
185
|
+
# Answers `contains`: element membership for a held Array, substring
|
|
186
|
+
# for anything else.
|
|
187
|
+
#
|
|
188
|
+
# `contains` means two different things depending on what is held —
|
|
189
|
+
# real element membership for a `list_of` field (a genuine Array
|
|
134
190
|
# arrives already, one element one member, nothing to split), and
|
|
135
|
-
# plain
|
|
136
|
-
# `members`' comma-split
|
|
137
|
-
#
|
|
138
|
-
# side's `instr`/`position` never
|
|
139
|
-
# moment a scalar's real content
|
|
191
|
+
# plain substring for anything else. The scalar case deliberately
|
|
192
|
+
# stays out of `members`' comma-split: that silently reads a
|
|
193
|
+
# free-text field's own comma as a separator — which the SQL
|
|
194
|
+
# side's `instr`/`position` never does, so the two disagree the
|
|
195
|
+
# moment a scalar's real content holds a comma. Matching SQL's
|
|
140
196
|
# substring reading here keeps every engine answering `contains`
|
|
141
197
|
# identically for the same declared field.
|
|
198
|
+
#
|
|
199
|
+
# @param held [Array, Object] the record's own value; anything but an Array is
|
|
200
|
+
# read through `to_s`
|
|
201
|
+
# @param want [Object] the element or substring looked for, compared as its `to_s`
|
|
202
|
+
# @return [Boolean] whether `held` has `want` as a member (Array) or a substring
|
|
142
203
|
def contains?(held, want)
|
|
143
204
|
return members(held).include?(want.to_s) if held.is_a?(Array)
|
|
144
205
|
|
|
145
206
|
held.to_s.include?(want.to_s)
|
|
146
207
|
end
|
|
147
208
|
|
|
148
|
-
#
|
|
149
|
-
#
|
|
209
|
+
# Answers `none_in_state` by looking the held identity up in another
|
|
210
|
+
# aggregate's repository and reading that record's state.
|
|
211
|
+
#
|
|
212
|
+
# **A cross-aggregate anti-join** — `where ref: { none_in_state:
|
|
213
|
+
# "Claim:held" }` holds when no record in the named aggregate,
|
|
150
214
|
# keyed by this record's own field value, is in the named state.
|
|
151
215
|
# No registry — no way to look the target up — reads as "not
|
|
152
216
|
# excluded", the same graceful default a missing record already
|
|
@@ -154,6 +218,17 @@ module Hecks
|
|
|
154
218
|
# loaded domain; ambiguity (two domains declaring one name) picks
|
|
155
219
|
# the first match rather than refusing, since a where-clause never
|
|
156
220
|
# raises (see `ordered?`).
|
|
221
|
+
#
|
|
222
|
+
# @param held [Object, nil] this record's own field value, used as the target
|
|
223
|
+
# record's identity
|
|
224
|
+
# @param want [String, Symbol] `"Aggregate:state"` — the target aggregate's bare
|
|
225
|
+
# name and the excluded state, split on the first colon
|
|
226
|
+
# @param registry [Runtime::Registry, nil] the booted registry to find the target
|
|
227
|
+
# aggregate and its repository in
|
|
228
|
+
# @return [Boolean] `false` only when the target record exists and its lifecycle
|
|
229
|
+
# field (or `:state`, absent a lifecycle) equals the named state; `true` when
|
|
230
|
+
# there is no registry, no such aggregate or no such record
|
|
231
|
+
# @raise [Runtime::WiringError] if the target aggregate's repository cannot be wired
|
|
157
232
|
def none_in_state?(held, want, registry)
|
|
158
233
|
return true unless registry
|
|
159
234
|
|
|
@@ -165,9 +240,42 @@ module Hecks
|
|
|
165
240
|
record = registry.repository(target_domain, target_ir).find(held)
|
|
166
241
|
return true unless record
|
|
167
242
|
|
|
168
|
-
|
|
243
|
+
# The field a state lives on, read from the target's own
|
|
244
|
+
# declaration — not assumed to be literally named `state`. Every
|
|
245
|
+
# `none_in_state` fixture this comparator originally shipped
|
|
246
|
+
# with (spec/query_none_in_state_*_spec.rb) happens to declare a
|
|
247
|
+
# plain `attribute :state, ...` rather than a real `lifecycle`,
|
|
248
|
+
# which is how the previous hardcoded `record.state[:state]`
|
|
249
|
+
# passed every one of them while being wrong for the shape this
|
|
250
|
+
# whole comparator exists to answer about: a real state machine.
|
|
251
|
+
# `lifecycle :field, default: ... do ... end` stores its state
|
|
252
|
+
# under `field` (`Instance#assign_creation_attributes`'s own
|
|
253
|
+
# `state[aggregate.lifecycle.field.to_sym] = ...`), and this
|
|
254
|
+
# codebase's own convention overwhelmingly names that field
|
|
255
|
+
# `status`, not `state` (`QualityControl::Clearance`'s own
|
|
256
|
+
# `lifecycle :status` among many others) — so the hardcoded key
|
|
257
|
+
# silently read `nil` from every real lifecycle-backed target,
|
|
258
|
+
# comparable(nil) != state was true unconditionally, and
|
|
259
|
+
# `none_in_state` against any lifecycle aggregate answered
|
|
260
|
+
# "not excluded" for every row, always, no matter its actual
|
|
261
|
+
# state. Found chasing `QualityControl::Bug.AwaitingClearance`
|
|
262
|
+
# (qa/bluebook/quality_control.bluebook), which is exactly this
|
|
263
|
+
# shape: `Clearance:green`/`Clearance:red` against a `lifecycle
|
|
264
|
+
# :status` aggregate. Falls back to `:state` when the target
|
|
265
|
+
# declares no lifecycle at all, so every existing fixture (a
|
|
266
|
+
# plain attribute literally named `state`) keeps answering
|
|
267
|
+
# exactly as before.
|
|
268
|
+
field = target_ir.lifecycle&.field || :state
|
|
269
|
+
comparable(record.state[field]) != state
|
|
169
270
|
end
|
|
170
271
|
|
|
272
|
+
# Searches every loaded domain for an aggregate by its bare name, taking
|
|
273
|
+
# the first match in the registry's load order.
|
|
274
|
+
#
|
|
275
|
+
# @param registry [Runtime::Registry] the booted registry whose bluebooks are searched
|
|
276
|
+
# @param name [String, nil] the aggregate's bare `hecks_name`, such as `"Claim"`
|
|
277
|
+
# @return [Array(String, Bluebook::Aggregate), nil] the owning domain's name and the
|
|
278
|
+
# aggregate; `nil` when no loaded domain declares one by that name
|
|
171
279
|
def find_aggregate_by_name(registry, name)
|
|
172
280
|
registry.bluebooks.each do |domain, bluebook|
|
|
173
281
|
aggregate = bluebook.aggregates.find { |a| a.hecks_name == name }
|