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
|
@@ -16,24 +16,34 @@ module Hecks
|
|
|
16
16
|
# historical bluebook text under old grammar defaults. Both are real,
|
|
17
17
|
# independent, currently-shipped consumers.
|
|
18
18
|
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
# `PostgresEra`'s own DB-held shapes
|
|
24
|
-
# it
|
|
25
|
-
#
|
|
26
|
-
# real orchestration, not resurrected from here.
|
|
19
|
+
# There is deliberately no top-level `check!`/`check_bluebook!` here
|
|
20
|
+
# walking a registry and reading/writing a held snapshot under
|
|
21
|
+
# `data/eras/*.bluebook`: it would duplicate, on its own, the same
|
|
22
|
+
# per-aggregate walk `CoverageCheck` already performs against
|
|
23
|
+
# `PostgresEra`'s own DB-held shapes, and nothing in production would
|
|
24
|
+
# call it — an unwired driver is not kept, per ADR 0032. If one is
|
|
25
|
+
# wanted, it is built informed by `CoverageCheck`'s real orchestration.
|
|
27
26
|
module EraGuard
|
|
28
27
|
extend ShapeDiff
|
|
29
28
|
|
|
30
29
|
module_function
|
|
31
30
|
|
|
31
|
+
# Refuses the boot when a held aggregate is gone and no translation says where it went.
|
|
32
|
+
#
|
|
32
33
|
# An aggregate that existed in the held text and answers to no
|
|
33
34
|
# current name — renamed silently, with nothing declaring `was:` to
|
|
34
35
|
# explain where its data went — is exactly the disease this guards
|
|
35
36
|
# against, and a plain per-aggregate diff would never see it: the
|
|
36
37
|
# current aggregate simply has no held counterpart to compare to.
|
|
38
|
+
#
|
|
39
|
+
# @param registry [Runtime::Registry] the registry whose declared translations are
|
|
40
|
+
# searched for a `was:` or a `retired` entry naming the held aggregate
|
|
41
|
+
# @param bluebook [Bluebook::Chapter] the bluebook booting now
|
|
42
|
+
# @param held_bluebook [Bluebook::Chapter] the bluebook parsed from the held era's text
|
|
43
|
+
# @return [void]
|
|
44
|
+
# @raise [Runtime::WiringError] if a held aggregate matches no current aggregate by
|
|
45
|
+
# name, no current aggregate's translation declares it as `was:`, and no translation
|
|
46
|
+
# for this domain retires it
|
|
37
47
|
def check_vanished_aggregates!(registry, bluebook, held_bluebook)
|
|
38
48
|
held_bluebook.aggregates.each do |held_aggregate|
|
|
39
49
|
claimed = bluebook.aggregates.any? do |aggregate|
|
|
@@ -54,9 +64,18 @@ module Hecks
|
|
|
54
64
|
end
|
|
55
65
|
end
|
|
56
66
|
|
|
67
|
+
# Raises the refusal naming every changed path no translation rule explains.
|
|
68
|
+
#
|
|
57
69
|
# The Layer-1 coverage refusal — one wording, shared with whoever
|
|
58
70
|
# calls it (today, `PostgresEra::LineageManager::CoverageCheck`'s
|
|
59
71
|
# own mint-time coverage check).
|
|
72
|
+
#
|
|
73
|
+
# @param bluebook [Bluebook::Chapter] the bluebook booting now, named in the message
|
|
74
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate whose shape changed
|
|
75
|
+
# @param uncovered [Array<String>] the unexplained paths, as `uncovered_attributes`
|
|
76
|
+
# returns them; must not be empty, since the first one seeds the suggested rule
|
|
77
|
+
# @return [void] never returns; always raises
|
|
78
|
+
# @raise [Runtime::WiringError] always, carrying the refusal wording
|
|
60
79
|
def refuse_uncovered!(bluebook, aggregate, uncovered)
|
|
61
80
|
raise WiringError,
|
|
62
81
|
"cannot boot #{bluebook.name}::#{aggregate.name}: its shape changed and " \
|
|
@@ -65,9 +84,18 @@ module Hecks
|
|
|
65
84
|
"#{suggestion(uncovered.first)}."
|
|
66
85
|
end
|
|
67
86
|
|
|
87
|
+
# Raises the refusal naming every new required attribute an existing record cannot fill.
|
|
88
|
+
#
|
|
68
89
|
# The addition-side sibling of refuse_uncovered! above — same
|
|
69
90
|
# wording shape, different cause: nothing vanished or changed type,
|
|
70
91
|
# something new arrived that an existing record has no way to hold.
|
|
92
|
+
#
|
|
93
|
+
# @param bluebook [Bluebook::Chapter] the bluebook booting now, named in the message
|
|
94
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate that gained the attributes
|
|
95
|
+
# @param unsafe [Array<Symbol>] the attribute names, as `unsafe_additions` returns them;
|
|
96
|
+
# must not be empty, since the first one seeds the suggested `backfill`
|
|
97
|
+
# @return [void] never returns; always raises
|
|
98
|
+
# @raise [Runtime::WiringError] always, carrying the refusal wording
|
|
71
99
|
def refuse_unsafe_addition!(bluebook, aggregate, unsafe)
|
|
72
100
|
raise WiringError,
|
|
73
101
|
"cannot boot #{bluebook.name}::#{aggregate.name}: #{unsafe.map { |name| ":#{name}" }.join(', ')} " \
|
|
@@ -77,8 +105,18 @@ module Hecks
|
|
|
77
105
|
"`backfill :#{unsafe.first}, default: ...`."
|
|
78
106
|
end
|
|
79
107
|
|
|
108
|
+
# Renders a path the way a translation file spells it.
|
|
109
|
+
#
|
|
110
|
+
# @param path [String] a bare attribute name or a dotted value-object member path
|
|
111
|
+
# @return [String] the path quoted (`"price.currency"`) when dotted, otherwise as a
|
|
112
|
+
# Symbol literal (`:cost`)
|
|
80
113
|
def render_path(path) = path.include?(".") ? path.inspect : ":#{path}"
|
|
81
114
|
|
|
115
|
+
# Proposes the translation rules that would explain one uncovered path.
|
|
116
|
+
#
|
|
117
|
+
# @param path [String] a bare attribute name or a dotted value-object member path
|
|
118
|
+
# @return [String] backticked example rules: `move`/`drop` for a dotted path,
|
|
119
|
+
# `rename`/`drop` for a bare name
|
|
82
120
|
def suggestion(path)
|
|
83
121
|
if path.include?(".")
|
|
84
122
|
"`move #{path.inspect}, to: #{path.inspect}` or `drop #{path.inspect}`"
|
|
@@ -90,60 +128,79 @@ module Hecks
|
|
|
90
128
|
# Parses held source into its own IR, in a scratch registry so a past
|
|
91
129
|
# era's text never touches the one actually booting.
|
|
92
130
|
#
|
|
93
|
-
#
|
|
94
|
-
# unconditionally
|
|
131
|
+
# Normal parse first, shadow only as a fallback — never shadow-parsing
|
|
132
|
+
# unconditionally. A handful of DSL
|
|
95
133
|
# defaults fork on `MetaValidator.shadow_parsing?` for a reason
|
|
96
|
-
# that has
|
|
134
|
+
# that has nothing to do with syntax the live grammar can no longer
|
|
97
135
|
# read at all (`identified_by { }`, `belongs_to`, `has_one`,
|
|
98
136
|
# `has_many` — genuinely removed spellings, exactly what shadow-
|
|
99
137
|
# parsing exists to keep readable): `reference_to`'s own default
|
|
100
138
|
# mint name (`default_reference_name`, attribute_collector.rb)
|
|
101
|
-
#
|
|
102
|
-
# applies even to text using nothing but current, live
|
|
103
|
-
# Held text minted under the
|
|
104
|
-
# this corpus today, since nothing has ever minted a second
|
|
105
|
-
# parses fine normally;
|
|
106
|
-
#
|
|
107
|
-
#
|
|
139
|
+
# is bare under ADR 0025 and `_id`-suffixed under shadow mode, and
|
|
140
|
+
# that fork applies even to text using nothing but current, live
|
|
141
|
+
# syntax. Held text minted under the current grammar — every real
|
|
142
|
+
# era in this corpus today, since nothing has ever minted a second
|
|
143
|
+
# one — parses fine normally; under unconditional shadow mode only
|
|
144
|
+
# the reference-naming default differs, so it silently
|
|
145
|
+
# reconstructs a different shape (and hash) than a fresh parse of
|
|
108
146
|
# the identical text — the same text hashing two different ways
|
|
109
147
|
# depending on which code path read it, breaking `ensure_named!`'s
|
|
110
148
|
# own from/to edge lookup with a spurious "no translation edge
|
|
111
149
|
# covers it" refusal that has nothing to do with any real
|
|
112
150
|
# translation gap.
|
|
113
151
|
#
|
|
114
|
-
# A normal parse can only ever
|
|
152
|
+
# A normal parse can only ever succeed on text the live grammar
|
|
115
153
|
# fully understands — there is no way for it to silently produce a
|
|
116
154
|
# wrong-but-plausible answer for genuinely legacy text, since every
|
|
117
155
|
# removed spelling refuses loudly (`Malformed`) rather than
|
|
118
156
|
# degrading. So: try normal first — if the ordinary grammar reads
|
|
119
|
-
# this text without complaint, that
|
|
157
|
+
# this text without complaint, that is the canonical, unambiguous
|
|
120
158
|
# interpretation, the same one `label_of`/`mint_hash` on the same
|
|
121
159
|
# source text always computes, whoever's asking. Only on a
|
|
122
160
|
# `Malformed` refusal — the one signal that actually means "this
|
|
123
161
|
# spelling doesn't exist anymore" — fall back to the legacy
|
|
124
|
-
# grammar
|
|
162
|
+
# grammar. Any other exception (a
|
|
125
163
|
# genuine syntax error, an unrelated validation refusal) propagates
|
|
126
164
|
# unchanged; swallowing it here to retry under shadow mode would
|
|
127
165
|
# risk masking a real defect in the held text behind a confusing
|
|
128
166
|
# second failure instead of the original, more specific one.
|
|
129
167
|
#
|
|
130
168
|
# `MetaValidator.while_shadow_parsing` (ADR 0025, docs/dsl-work-
|
|
131
|
-
# slices.md's S0a) is what makes the fallback a
|
|
169
|
+
# slices.md's S0a) is what makes the fallback a legacy grammar
|
|
132
170
|
# rather than just a second copy of today's: it stops
|
|
133
171
|
# `BluebookBuilder.build` from judging this text against the
|
|
134
|
-
# grammar as it stands
|
|
135
|
-
# a removed spelling refuse
|
|
172
|
+
# grammar as it stands now, which is the one thing that would make
|
|
173
|
+
# a removed spelling refuse history the day it is removed from
|
|
136
174
|
# live source. The scratch registry is throwaway either way —
|
|
137
175
|
# nothing here is dispatched against or exposed to the real one —
|
|
138
176
|
# so skipping the judge/assemble round-trip changes nothing this
|
|
139
177
|
# method reads: `shape`, `uncovered_attributes`, and friends only
|
|
140
178
|
# ever ask the built IR for its own structure.
|
|
179
|
+
#
|
|
180
|
+
# @param source [String] the held bluebook text to evaluate
|
|
181
|
+
# @param path [String] the file path the text is evaluated as, which the predicate
|
|
182
|
+
# extractor reads from disk; callers pass a tempfile holding the same text
|
|
183
|
+
# @return [Bluebook::Chapter, nil] the first bluebook the text declares; nil when it
|
|
184
|
+
# declares none
|
|
185
|
+
# @raise [Bluebook::DSL::Malformed] if the text parses under neither the live grammar
|
|
186
|
+
# nor the legacy one
|
|
187
|
+
# @raise [SyntaxError] if the text is not valid Ruby
|
|
141
188
|
def shadow_parse(source, path)
|
|
142
189
|
parse_bluebook(source, path, shadow: false)
|
|
143
190
|
rescue Hecks::Bluebook::DSL::Malformed
|
|
144
191
|
parse_bluebook(source, path, shadow: true)
|
|
145
192
|
end
|
|
146
193
|
|
|
194
|
+
# Evaluates bluebook text once, in a throwaway registry, under one chosen grammar.
|
|
195
|
+
#
|
|
196
|
+
# @param source [String] the bluebook text to evaluate
|
|
197
|
+
# @param path [String] the file path reported to `Kernel.eval` as the text's origin
|
|
198
|
+
# @param shadow [Boolean] true evaluates inside `MetaValidator.while_shadow_parsing`
|
|
199
|
+
# (the legacy grammar); false evaluates under the live grammar
|
|
200
|
+
# @return [Bluebook::Chapter, nil] the first bluebook the text registered in the scratch
|
|
201
|
+
# registry; nil when it declares none
|
|
202
|
+
# @raise [Bluebook::DSL::Malformed] if the chosen grammar refuses the text
|
|
203
|
+
# @raise [SyntaxError] if the text is not valid Ruby
|
|
147
204
|
def parse_bluebook(source, path, shadow:)
|
|
148
205
|
scratch = Registry.new
|
|
149
206
|
loading = Ports::Loading.bootstrap
|
|
@@ -6,41 +6,52 @@ require_relative "storage_shape"
|
|
|
6
6
|
module Hecks
|
|
7
7
|
module Runtime
|
|
8
8
|
# The refusal wording for a held era whose digest no longer matches its
|
|
9
|
-
# frozen text — the digest mismatch alone is what
|
|
9
|
+
# frozen text — the digest mismatch alone is what detects tampering (a
|
|
10
10
|
# plain SHA256 comparison over raw bytes, unrelated to any of this);
|
|
11
11
|
# this only supplies the wording once that's already fired.
|
|
12
12
|
#
|
|
13
|
-
#
|
|
13
|
+
# Lives here, not under `Ports::Persistence` — `project` below directly
|
|
14
14
|
# calls `Runtime::EraGuard.shadow_parse`/`Runtime::StorageShape.project`
|
|
15
15
|
# (DSL-execution machinery), the "an adapter/port reaches into the
|
|
16
16
|
# runtime instead of being handed already-computed data" shape that
|
|
17
17
|
# has caused trouble here before. A capability this dependent on the
|
|
18
|
-
# runtime is a runtime-owned one that the Postgres adapter
|
|
18
|
+
# runtime is a runtime-owned one that the Postgres adapter calls, not
|
|
19
19
|
# a ports-level module that happens to reach sideways into it.
|
|
20
20
|
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
# already, not a normal boot path.
|
|
21
|
+
# Every tamper refusal reaches the same generic wording, cosmetic edit
|
|
22
|
+
# or real shape change alike: telling the two apart by re-parsing the
|
|
23
|
+
# edited text is a pure quality-of-message nicety, not a safety
|
|
24
|
+
# property, and an edit that cannot be classified has to fall to the
|
|
25
|
+
# generic wording anyway. An operator judges "did this matter"
|
|
26
|
+
# themselves, reading the still-archived original — an anomalous
|
|
27
|
+
# recovery moment already, not a normal boot path.
|
|
29
28
|
module EraTamper
|
|
30
29
|
module_function
|
|
31
30
|
|
|
31
|
+
# Words the boot refusal for a held era text whose digest no longer matches.
|
|
32
|
+
#
|
|
33
|
+
# @param domain [String] name of the domain whose era was edited
|
|
34
|
+
# @param ordinal [Integer] the edited era's ordinal in `hecks_eras`
|
|
35
|
+
# @return [String] the refusal message, ready to raise as a `Runtime::WiringError`
|
|
32
36
|
def refusal(domain:, ordinal:)
|
|
33
37
|
"cannot boot #{domain}: the held text of era #{ordinal} was edited after it was frozen — " \
|
|
34
38
|
"held era texts are storage facts; restore the original text, or reset the data"
|
|
35
39
|
end
|
|
36
40
|
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
41
|
+
# Parses a bluebook text and returns its storage-shape projection.
|
|
42
|
+
#
|
|
43
|
+
# The projection is JSON-normalized for structural comparison
|
|
44
|
+
# against a stored projection; nil when the text does not load.
|
|
45
|
+
# Always parsed through a fresh tempfile, never the held file's own
|
|
46
|
+
# path: the predicate extractor caches source by path, and a held
|
|
47
|
+
# path whose content has changed (the very situation this module
|
|
48
|
+
# exists for) would hand it stale lines.
|
|
49
|
+
#
|
|
50
|
+
# @param text [String] the bluebook source to parse
|
|
51
|
+
# @param _source_path [String, nil] ignored; the text is always parsed from a tempfile
|
|
52
|
+
# @return [Hash{String => Object}, nil] `StorageShape.project`'s Hash after a JSON
|
|
53
|
+
# round-trip; nil when the text declares no bluebook, or when parsing or projecting
|
|
54
|
+
# raises any `StandardError` or `SyntaxError`
|
|
44
55
|
def project(text, _source_path = nil)
|
|
45
56
|
file = Tempfile.new(["hecks-tamper-", ".bluebook"])
|
|
46
57
|
begin
|
|
@@ -15,9 +15,17 @@ module Hecks
|
|
|
15
15
|
# `ancestor_name` is the declared name a rename came from (matches
|
|
16
16
|
# the held bluebook's own aggregate names); `ancestor_storage_name`
|
|
17
17
|
# is its derived, snake_case file/table name. Both are nil unless
|
|
18
|
-
# the
|
|
18
|
+
# the edge declares a `was:` name that differs from the aggregate's own.
|
|
19
19
|
attr_reader :ancestor_name, :ancestor_storage_name
|
|
20
20
|
|
|
21
|
+
# Builds the rules of the first declared translation that mentions an aggregate.
|
|
22
|
+
#
|
|
23
|
+
# @param registry [Runtime::Registry] the registry whose declared translations are
|
|
24
|
+
# searched, in declaration order
|
|
25
|
+
# @param domain [String, Symbol] name of the domain the aggregate belongs to
|
|
26
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate as currently declared
|
|
27
|
+
# @return [Ports::Persistence::Lineage, nil] the rules of the first translation for `domain`
|
|
28
|
+
# that names the aggregate; nil when none does
|
|
21
29
|
def self.for(registry, domain, aggregate)
|
|
22
30
|
translation = registry.translations.find do |candidate|
|
|
23
31
|
candidate.domain == domain.to_s && candidate.for_aggregate(aggregate.name)
|
|
@@ -27,9 +35,18 @@ module Hecks
|
|
|
27
35
|
from_declared(translation.for_aggregate(aggregate.name), aggregate.name)
|
|
28
36
|
end
|
|
29
37
|
|
|
30
|
-
#
|
|
38
|
+
# Builds the rules one declared translation edge carries for one aggregate.
|
|
39
|
+
#
|
|
40
|
+
# One specific edge's rules for one aggregate — what the mint
|
|
31
41
|
# path uses, where `for` would happily answer with whichever
|
|
32
42
|
# edge in the registry mentioned the aggregate first.
|
|
43
|
+
#
|
|
44
|
+
# @param declared [Bluebook::TranslationAggregate, nil] the edge's entry for the
|
|
45
|
+
# aggregate, as `Bluebook::Translation#for_aggregate` returns it; nil when the edge
|
|
46
|
+
# does not mention the aggregate
|
|
47
|
+
# @param aggregate_name [String, Symbol] the aggregate's current name, compared with
|
|
48
|
+
# `declared.was` to decide whether the edge renames the aggregate itself
|
|
49
|
+
# @return [Ports::Persistence::Lineage, nil] the edge's rules; nil when `declared` is nil
|
|
33
50
|
def self.from_declared(declared, aggregate_name)
|
|
34
51
|
return nil unless declared
|
|
35
52
|
|
|
@@ -43,6 +60,24 @@ module Hecks
|
|
|
43
60
|
ancestor_name: ancestor_name, ancestor_storage_name: ancestor_storage_name)
|
|
44
61
|
end
|
|
45
62
|
|
|
63
|
+
# @param renames [Hash{Symbol => Symbol}] old top-level attribute name to new name
|
|
64
|
+
# @param moves [Array<Bluebook::TranslationMove>] fields crossing a value-object
|
|
65
|
+
# boundary, each a `from`/`to` pair of bare or dotted paths
|
|
66
|
+
# @param converts [Array<Bluebook::TranslationConvert>] moves whose value is replaced
|
|
67
|
+
# through an exhaustive `values` lookup table
|
|
68
|
+
# @param drops [Array<Symbol>] bare or dotted paths whose data is deliberately discarded
|
|
69
|
+
# @param retypes [Array<Bluebook::TranslationRetype>] value-object or entity type names
|
|
70
|
+
# declared to mean the same shape
|
|
71
|
+
# @param computes [Array<Bluebook::TranslationCompute>] SQL-only rules, never applied
|
|
72
|
+
# in process
|
|
73
|
+
# @param rekeys [Array<Bluebook::TranslationRekey>] SQL-only identity rewrites; only
|
|
74
|
+
# the first is read
|
|
75
|
+
# @param backfills [Array<Bluebook::TranslationBackfill>] defaults for new top-level
|
|
76
|
+
# attributes an old entry lacks
|
|
77
|
+
# @param ancestor_name [String, nil] the aggregate's declared name before the edge;
|
|
78
|
+
# nil unless the edge renames the aggregate itself
|
|
79
|
+
# @param ancestor_storage_name [String, nil] snake_case storage name of
|
|
80
|
+
# `ancestor_name`; nil unless the edge renames the aggregate itself
|
|
46
81
|
def initialize(renames, moves = [], converts = [], drops = [], retypes: [], computes: [], rekeys: [],
|
|
47
82
|
backfills: [], ancestor_name: nil, ancestor_storage_name: nil)
|
|
48
83
|
@renames = renames
|
|
@@ -57,37 +92,60 @@ module Hecks
|
|
|
57
92
|
@ancestor_storage_name = ancestor_storage_name
|
|
58
93
|
end
|
|
59
94
|
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
#
|
|
95
|
+
# Reports whether any rule in this edge is a compute.
|
|
96
|
+
#
|
|
97
|
+
# Compute is the one rule kind with no in-process implementation at
|
|
98
|
+
# all. An aggregate carrying one refuses to boot anywhere but
|
|
99
|
+
# Postgres, per-rule and by name, before the general drift
|
|
100
|
+
# machinery says anything vaguer.
|
|
101
|
+
#
|
|
102
|
+
# @return [Boolean] true when the edge declares at least one compute rule
|
|
64
103
|
def computes? = !@computes.empty?
|
|
65
104
|
|
|
66
|
-
#
|
|
105
|
+
# Reports whether this edge rewrites the aggregate's identity.
|
|
106
|
+
#
|
|
107
|
+
# The single source of truth for "does this edge rekey this
|
|
67
108
|
# aggregate" — every consumer (coverage_check.rb's identity gate,
|
|
68
109
|
# minter.rb's approval gate, layer_two.rb's audit, head_compiler.rb's
|
|
69
|
-
# SQL compilation) asks
|
|
110
|
+
# SQL compilation) asks this, never re-derives it from `declared`
|
|
70
111
|
# independently. One accessor to change if what a rekey rule means
|
|
71
112
|
# ever needs to change, not four call sites in four files.
|
|
113
|
+
#
|
|
114
|
+
# @return [Boolean] true when the edge declares at least one rekey rule
|
|
72
115
|
def rekey? = !@rekeys.empty?
|
|
73
116
|
|
|
117
|
+
# Returns the SQL expression the edge's first rekey rule declares.
|
|
118
|
+
#
|
|
74
119
|
# The rekey's own SQL — first-and-only rule, same one-per-aggregate
|
|
75
120
|
# assumption `compute` makes about its own list where it matters
|
|
76
121
|
# (an edge with more than one is a DSL-level decision, not
|
|
77
122
|
# something this reader arbitrates).
|
|
123
|
+
#
|
|
124
|
+
# @return [String, nil] the first rekey rule's SQL; nil when the edge declares no rekey
|
|
78
125
|
def rekey_sql = @rekeys.first&.sql
|
|
79
126
|
|
|
80
|
-
#
|
|
127
|
+
# Rewrites one journal entry's state from the held shape into the current one.
|
|
128
|
+
#
|
|
129
|
+
# The reference semantics for the five portable rule kinds —
|
|
81
130
|
# rename, move, convert, drop, and the aggregate-level `was:`.
|
|
82
131
|
# `retype` moves nothing (stored state never carries a type name)
|
|
83
132
|
# and `compute` is deliberately not applied here: its SQL is its
|
|
84
133
|
# only implementation, so this transform neither imitates nor
|
|
85
134
|
# checks it — the source field passes through untouched, and the
|
|
86
135
|
# audit verifies compute output against the matview alone.
|
|
136
|
+
#
|
|
137
|
+
# @param entry [Ports::Persistence::Entry] the entry as stored: `state` has Symbol
|
|
138
|
+
# top-level keys and String keys inside a nested value-object Hash
|
|
139
|
+
# @return [Ports::Persistence::Entry] a new entry with a deep-copied, translated state
|
|
140
|
+
# and the same `operation`, `id` and `mirrors`; `entry` itself, untouched, when it
|
|
141
|
+
# is not a save or carries no state
|
|
142
|
+
# @raise [Runtime::WiringError] if a convert meets a value missing from its `values`
|
|
143
|
+
# table, or a move or convert would nest under a destination already holding a
|
|
144
|
+
# non-Hash value
|
|
87
145
|
def translate(entry)
|
|
88
146
|
return entry unless entry.save? && entry.state
|
|
89
147
|
|
|
90
|
-
# Deep, not shallow: a move or convert reaches
|
|
148
|
+
# Deep, not shallow: a move or convert reaches into a nested
|
|
91
149
|
# value-object hash, and a shallow dup would quietly mutate
|
|
92
150
|
# the caller's copy of the original entry.
|
|
93
151
|
state = deep_dup(entry.state)
|
|
@@ -95,24 +153,26 @@ module Hecks
|
|
|
95
153
|
@moves.each { |move| apply_move(state, move) }
|
|
96
154
|
@converts.each { |convert| apply_convert(state, convert) }
|
|
97
155
|
@drops.each { |name| apply_drop(state, name) }
|
|
98
|
-
#
|
|
156
|
+
# Last, and only where nothing already answered — a backfill
|
|
99
157
|
# fills the gap a rename/move/convert left untouched, never
|
|
100
158
|
# overwrites a value that already made it across.
|
|
101
159
|
@backfills.each { |backfill| state[backfill.name] = backfill.default unless state.key?(backfill.name) }
|
|
102
160
|
Entry.new(operation: entry.operation, id: entry.id, state: state, mirrors: entry.mirrors)
|
|
103
161
|
end
|
|
104
162
|
|
|
163
|
+
# Reports whether some rule accounts for a held path that vanished or changed type.
|
|
164
|
+
#
|
|
105
165
|
# Whether this translation names `path` as an old key it accounts
|
|
106
166
|
# for — the rename, move, or convert it came from, or an explicit
|
|
107
167
|
# drop. `path` is a bare name ("cost") or a dotted value-object
|
|
108
|
-
# member ("price.currency"); a rule covering the
|
|
168
|
+
# member ("price.currency"); a rule covering the whole top-level
|
|
109
169
|
# attribute (a rename, a top-level move/convert/drop) also covers
|
|
110
170
|
# anything nested under it, since the whole value travels or goes
|
|
111
|
-
# away together.
|
|
112
|
-
# member — that vanished (or silently changed type) without
|
|
113
|
-
# anything explaining it, even when some
|
|
171
|
+
# away together. This is what catches a field — or a value object's
|
|
172
|
+
# own member — that vanished (or silently changed type) without
|
|
173
|
+
# anything explaining it, even when some other field is covered.
|
|
114
174
|
#
|
|
115
|
-
# `backfills` matches on the
|
|
175
|
+
# `backfills` matches on the whole name only, never a dotted
|
|
116
176
|
# prefix — a backfill names a top-level attribute that is new
|
|
117
177
|
# outright (nothing to be a prefix of on the held side), unlike
|
|
118
178
|
# every rule above it, which explains a path that existed and
|
|
@@ -121,9 +181,14 @@ module Hecks
|
|
|
121
181
|
# One `||` chain over a closed, fixed set of rule kinds (renames,
|
|
122
182
|
# moves, converts, drops, computes, backfills) — the same six this
|
|
123
183
|
# file's other methods enumerate. Splitting each disjunct into its
|
|
124
|
-
# own predicate would scatter one question ("does
|
|
184
|
+
# own predicate would scatter one question ("does any rule explain
|
|
125
185
|
# this path") across six same-shaped methods with nothing else to
|
|
126
186
|
# do.
|
|
187
|
+
#
|
|
188
|
+
# @param path [String, Symbol] a bare attribute name or a dotted value-object member
|
|
189
|
+
# path on the held side
|
|
190
|
+
# @return [Boolean] true when a rename, move, convert, drop or compute names the path or
|
|
191
|
+
# its top-level attribute as its source, or a backfill names the top-level attribute
|
|
127
192
|
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
128
193
|
# rubocop:disable-next Metrics/PerceivedComplexity
|
|
129
194
|
def explains?(path)
|
|
@@ -138,31 +203,36 @@ module Hecks
|
|
|
138
203
|
@backfills.any? { |backfill| backfill.name.to_s == top }
|
|
139
204
|
end
|
|
140
205
|
|
|
141
|
-
#
|
|
142
|
-
#
|
|
206
|
+
# Reports whether some rule gives an existing record a value at a new attribute.
|
|
207
|
+
#
|
|
208
|
+
# The destination-side twin of `explains?` above, which only ever
|
|
209
|
+
# asks about a rule's source. `unsafe_additions` asks a different
|
|
143
210
|
# question — not "was this vanished path accounted for" but "does
|
|
144
211
|
# an existing record end up with a value here" — and a move or
|
|
145
|
-
# convert whose `to:` lands a old field inside a
|
|
212
|
+
# convert whose `to:` lands a old field inside a brand-new
|
|
146
213
|
# top-level attribute (`weight` becoming `contents.weight` when
|
|
147
214
|
# `Contents` did not exist before) fills that attribute for an
|
|
148
215
|
# existing record exactly as a `backfill` would, even though
|
|
149
216
|
# nothing named `contents` explains any vanished path. `compute`
|
|
150
217
|
# counts on the same terms `explains?` already grants it
|
|
151
218
|
# elsewhere in this file — Postgres-only and audited, not
|
|
152
|
-
# actually applied by
|
|
219
|
+
# actually applied by this method, the same gap the vanish side
|
|
153
220
|
# already lives with.
|
|
154
221
|
#
|
|
155
|
-
# `@renames.value?` belongs here too
|
|
156
|
-
#
|
|
157
|
-
#
|
|
158
|
-
#
|
|
159
|
-
#
|
|
160
|
-
#
|
|
161
|
-
#
|
|
162
|
-
#
|
|
163
|
-
#
|
|
164
|
-
#
|
|
165
|
-
#
|
|
222
|
+
# `@renames.value?` belongs here too: a bare
|
|
223
|
+
# `rename :cost, to: :amount` is the plainest possible covering
|
|
224
|
+
# rule there is (`translate` above applies it unconditionally, no
|
|
225
|
+
# lookup table, no per-record ambiguity — simpler than a move or
|
|
226
|
+
# convert), and without it `unsafe_additions` reports the new name
|
|
227
|
+
# as an unexplained required addition on every rename-only edge,
|
|
228
|
+
# the single most common translation shape there is. `explains?`
|
|
229
|
+
# checks the source side (`@renames.key?`); this is the symmetric
|
|
230
|
+
# destination-side check.
|
|
231
|
+
#
|
|
232
|
+
# @param path [String, Symbol] the name of a top-level attribute new in the current
|
|
233
|
+
# shape
|
|
234
|
+
# @return [Boolean] true when a rename, move, convert or compute lands a value in that
|
|
235
|
+
# attribute, or a backfill names it
|
|
166
236
|
def fills?(path)
|
|
167
237
|
path = path.to_s
|
|
168
238
|
|
|
@@ -173,11 +243,18 @@ module Hecks
|
|
|
173
243
|
@backfills.any? { |backfill| backfill.name.to_s == path }
|
|
174
244
|
end
|
|
175
245
|
|
|
176
|
-
#
|
|
177
|
-
#
|
|
246
|
+
# Reports whether a declared retype pairs two type names as the same shape.
|
|
247
|
+
#
|
|
248
|
+
# A retype covers a value object or entity whose own name changed
|
|
178
249
|
# with its members intact. Nothing in the stored data carries the
|
|
179
250
|
# type name, so this never moves a value; it only satisfies the
|
|
180
251
|
# era diff's literal type-name comparison.
|
|
252
|
+
#
|
|
253
|
+
# @param held_type [String, Bluebook::Reference] the type the held era declares,
|
|
254
|
+
# compared by its `to_s`
|
|
255
|
+
# @param current_type [String, Bluebook::Reference] the type declared now, compared by
|
|
256
|
+
# its `to_s`
|
|
257
|
+
# @return [Boolean] true when some retype rule runs from `held_type` to `current_type`
|
|
181
258
|
def retype?(held_type, current_type)
|
|
182
259
|
@retypes.any? { |retype| retype.from == held_type.to_s && retype.to == current_type.to_s }
|
|
183
260
|
end
|
|
@@ -193,25 +270,25 @@ module Hecks
|
|
|
193
270
|
end
|
|
194
271
|
|
|
195
272
|
# M27 (docs/audits/2026-08-10-main-bug-audit.md,
|
|
196
|
-
# docs/audits/2026-08-11-bug-triage.md) —
|
|
273
|
+
# docs/audits/2026-08-11-bug-triage.md) — simultaneous, not
|
|
197
274
|
# sequential: `state[new] = state.delete(old)` per rename, run
|
|
198
|
-
# one rule at a time against the
|
|
275
|
+
# one rule at a time against the same hash it was reading from,
|
|
199
276
|
# loses data the instant one rule's destination is another
|
|
200
|
-
# rule's source.
|
|
201
|
-
# `rename :b, to: :a`) on `{a: 1, b: 2}`
|
|
202
|
-
# `{a: 1}` — the first rule
|
|
203
|
-
# before the second rule ever
|
|
204
|
-
# value the whole edge
|
|
205
|
-
# `:a`)
|
|
206
|
-
# key and value from `state`
|
|
207
|
-
# and only
|
|
277
|
+
# rule's source. Applied sequentially, a swap (`rename :a, to: :b`
|
|
278
|
+
# alongside `rename :b, to: :a`) on `{a: 1, b: 2}` produces
|
|
279
|
+
# `{a: 1}` — the first rule writes `b: 1` over the real `b: 2`
|
|
280
|
+
# before the second rule ever gets a chance to read it, and the
|
|
281
|
+
# value the whole edge is supposed to preserve (2, moved to
|
|
282
|
+
# `:a`) is gone. The standard fix: snapshot every rule's old
|
|
283
|
+
# key and value from `state` first, then remove every old key
|
|
284
|
+
# and only then write every new key — a rename never reads a
|
|
208
285
|
# key this same pass has already written to, so a swap or a
|
|
209
286
|
# longer chain applies as one permutation, not a sequence of
|
|
210
287
|
# edits each stepping on the last.
|
|
211
288
|
def apply_renames(state, renames)
|
|
212
289
|
snapshot = renames.filter_map { |old_name, new_name| [old_name, new_name, state[old_name]] if state.key?(old_name) }
|
|
213
290
|
snapshot.each { |old_name, _new_name, _value| state.delete(old_name) }
|
|
214
|
-
#
|
|
291
|
+
# Not combinable (Style/CombinableLoops is disabled repo-wide, see
|
|
215
292
|
# .rubocop.yml, for exactly this reason): a swap (:a<->:b) needs
|
|
216
293
|
# every delete done before any write, or the first rename's write
|
|
217
294
|
# becomes the second rename's delete target — see this method's
|
|
@@ -236,8 +313,16 @@ module Hecks
|
|
|
236
313
|
end
|
|
237
314
|
|
|
238
315
|
# A dotted path's first segment is a top-level (symbol) key; a
|
|
239
|
-
# second segment reaches into a value-object member
|
|
240
|
-
#
|
|
316
|
+
# second segment reaches into a value-object member by its string
|
|
317
|
+
# key — the spelling a raw stored row carries. Translation runs on
|
|
318
|
+
# raw rows, before the state codec decodes anything (PR A3): its
|
|
319
|
+
# only caller (`Translation::Audit::LayerTwo`) feeds it
|
|
320
|
+
# head-snapshot rows straight out of `JSON.parse`, and the
|
|
321
|
+
# PostgresEra head applies the same rules in SQL before
|
|
322
|
+
# `PostgresEra#decode` ever sees the jsonb. Decode is always the
|
|
323
|
+
# last step, so an undeclared (retired) member this rule has to
|
|
324
|
+
# read is still exactly as it was written, and never something an
|
|
325
|
+
# adapter's `entries` — decoded, deep-symbol — would be fed here.
|
|
241
326
|
def apply_move(state, move)
|
|
242
327
|
old_top, old_member = move.from.split(".", 2)
|
|
243
328
|
new_top, new_member = move.to.split(".", 2)
|
|
@@ -282,19 +367,18 @@ module Hecks
|
|
|
282
367
|
[value, true]
|
|
283
368
|
end
|
|
284
369
|
|
|
285
|
-
#
|
|
286
|
-
# top segment
|
|
287
|
-
# stored as a bare scalar id —
|
|
288
|
-
#
|
|
289
|
-
#
|
|
290
|
-
#
|
|
291
|
-
#
|
|
292
|
-
#
|
|
293
|
-
#
|
|
294
|
-
#
|
|
295
|
-
#
|
|
296
|
-
#
|
|
297
|
-
# raises the identical wording.
|
|
370
|
+
# Adversarial finding, not a hypothetical: a destination whose
|
|
371
|
+
# top segment already holds a value — most commonly a reference,
|
|
372
|
+
# stored as a bare scalar id — must not be nested under when a
|
|
373
|
+
# dotted destination needs it (`state[top] ||= {}` alone only
|
|
374
|
+
# guards nil/false, so a truthy non-Hash sails straight through
|
|
375
|
+
# to `state[top][member] =`, i.e. `"team-1"["detail"] =`, which
|
|
376
|
+
# is String#[]= and raises an unrelated-looking IndexError).
|
|
377
|
+
# Whether it crashes or silently replaces the value, that is a
|
|
378
|
+
# `drop` that never declared itself — the one thing this language
|
|
379
|
+
# exists to make explicit (see apply_convert's own refusal above,
|
|
380
|
+
# the same shape). Refuse by name instead, on both sides: the SQL
|
|
381
|
+
# half (hecks_tr_insert) raises the identical wording.
|
|
298
382
|
def insert(state, top, member, value, rule:)
|
|
299
383
|
return state[top] = value unless member
|
|
300
384
|
|