hecks 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/hecks/adapters/driven/claude_code.rb +7 -7
- data/lib/hecks/adapters/driven/d1.rb +187 -23
- data/lib/hecks/adapters/driven/folder.rb +10 -10
- data/lib/hecks/adapters/driven/google_authentication.rb +8 -8
- data/lib/hecks/adapters/driven/governance_authorization.rb +31 -13
- data/lib/hecks/adapters/driven/heki/journal.rb +5 -4
- data/lib/hecks/adapters/driven/heki/saga_store.rb +5 -5
- data/lib/hecks/adapters/driven/heki.rb +13 -7
- data/lib/hecks/adapters/driven/identity_registry.rb +2 -2
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +3 -3
- data/lib/hecks/adapters/driven/lambda/client.rb +34 -9
- data/lib/hecks/adapters/driven/lambda.rb +39 -33
- data/lib/hecks/adapters/driven/local_storage.rb +17 -10
- data/lib/hecks/adapters/driven/memory.rb +205 -9
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +1 -1
- data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
- data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
- data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
- data/lib/hecks/adapters/driven/postgres.rb +175 -28
- data/lib/hecks/adapters/driven/prism.rb +4 -4
- data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
- data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
- data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
- data/lib/hecks/adapters/driven/sqlite.rb +181 -21
- data/lib/hecks/adapters/driven.rb +4 -4
- data/lib/hecks/adapters/driving/github_webhook.rb +18 -18
- data/lib/hecks/behaviors/dsl.rb +2 -2
- data/lib/hecks/behaviors/expectations.rb +23 -23
- data/lib/hecks/behaviors/ir.rb +1 -1
- data/lib/hecks/behaviors/rspec.rb +1 -1
- data/lib/hecks/behaviors/runner.rb +2 -2
- data/lib/hecks/behaviors.rb +1 -1
- data/lib/hecks/bluebook/aggregate.rb +13 -13
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +10 -10
- data/lib/hecks/bluebook/assembly/build.rb +1 -1
- data/lib/hecks/bluebook/assembly/contract.rb +39 -16
- data/lib/hecks/bluebook/assembly/contracts.rb +40 -38
- data/lib/hecks/bluebook/assembly/marks.rb +18 -18
- data/lib/hecks/bluebook/assembly/specializer.rb +19 -21
- data/lib/hecks/bluebook/assembly.rb +9 -9
- data/lib/hecks/bluebook/attribute.rb +9 -9
- data/lib/hecks/bluebook/behaviour/aggregate.rb +11 -11
- data/lib/hecks/bluebook/behaviour/attribute.rb +5 -5
- data/lib/hecks/bluebook/behaviour/chapter.rb +23 -5
- data/lib/hecks/bluebook/behaviour/command.rb +23 -23
- data/lib/hecks/bluebook/behaviour/domain_port.rb +7 -7
- data/lib/hecks/bluebook/behaviour/entity.rb +8 -8
- data/lib/hecks/bluebook/behaviour/hexagon.rb +4 -4
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +5 -5
- data/lib/hecks/bluebook/behaviour/policy.rb +12 -12
- data/lib/hecks/bluebook/behaviour/process_manager.rb +7 -7
- data/lib/hecks/bluebook/behaviour/query.rb +1 -1
- data/lib/hecks/bluebook/behaviour/read_model.rb +8 -8
- data/lib/hecks/bluebook/behaviour/traits.rb +12 -12
- data/lib/hecks/bluebook/behaviour/value_object.rb +6 -6
- data/lib/hecks/bluebook/capabilities.rb +27 -0
- data/lib/hecks/bluebook/chapter.rb +28 -9
- data/lib/hecks/bluebook/command.rb +12 -12
- data/lib/hecks/bluebook/domain_port.rb +9 -9
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +96 -96
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +41 -41
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +22 -2
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +111 -74
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +48 -30
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +103 -103
- data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
- data/lib/hecks/bluebook/dsl/entity_builder.rb +56 -56
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +89 -30
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +17 -17
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
- data/lib/hecks/bluebook/dsl/policy_builder.rb +30 -21
- data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +35 -35
- data/lib/hecks/bluebook/dsl/query_builder.rb +5 -5
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +34 -34
- data/lib/hecks/bluebook/dsl/rule_reference.rb +41 -39
- data/lib/hecks/bluebook/dsl/translation_builder.rb +9 -9
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +16 -16
- data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
- data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
- data/lib/hecks/bluebook/entity.rb +11 -11
- data/lib/hecks/bluebook/expression/ast_json.rb +20 -20
- data/lib/hecks/bluebook/expression/ast_reader.rb +3 -3
- data/lib/hecks/bluebook/expression/canonical_form.rb +9 -9
- data/lib/hecks/bluebook/expression/evaluator.rb +18 -18
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +18 -18
- data/lib/hecks/bluebook/expression/resolver.rb +60 -62
- data/lib/hecks/bluebook/hexagon.rb +1 -1
- data/lib/hecks/bluebook/lifecycle.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/judge.rb +101 -87
- data/lib/hecks/bluebook/meta_validator/plan.rb +39 -39
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -2
- data/lib/hecks/bluebook/meta_validator/readings.rb +43 -43
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +44 -37
- data/lib/hecks/bluebook/meta_validator/shapes.rb +25 -21
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +145 -31
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +6 -6
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +5 -5
- data/lib/hecks/bluebook/meta_validator.rb +70 -70
- data/lib/hecks/bluebook/model_check.rb +170 -84
- data/lib/hecks/bluebook/pattern_subset.rb +9 -9
- data/lib/hecks/bluebook/policy.rb +15 -13
- data/lib/hecks/bluebook/process_manager.rb +14 -14
- data/lib/hecks/bluebook/project_register.rb +6 -6
- data/lib/hecks/bluebook/query.rb +4 -4
- data/lib/hecks/bluebook/read_model.rb +14 -14
- data/lib/hecks/bluebook/reference.rb +8 -8
- data/lib/hecks/bluebook/smoke_test.rb +19 -19
- data/lib/hecks/bluebook/synthesizer.rb +12 -12
- data/lib/hecks/bluebook/translation.rb +4 -4
- data/lib/hecks/bluebook/value_object.rb +6 -6
- data/lib/hecks/bluebook.rb +2 -2
- data/lib/hecks/codemod/legacy_dispatch_args.rb +299 -0
- data/lib/hecks/codemod/legacy_dispatch_recorder.rb +186 -0
- data/lib/hecks/codemod.rb +36 -35
- data/lib/hecks/construct.rb +6 -6
- data/lib/hecks/corpus.rb +317 -0
- data/lib/hecks/deprecation.rb +95 -0
- data/lib/hecks/doc/reference.rb +19 -19
- data/lib/hecks/embryonaut_bluebook.rb +11 -11
- data/lib/hecks/facade/cli_door.rb +69 -10
- data/lib/hecks/facade/cli_runner.rb +105 -24
- data/lib/hecks/facade/command_request.rb +23 -0
- data/lib/hecks/facade/handle.rb +79 -32
- data/lib/hecks/facade/json_door.rb +106 -25
- data/lib/hecks/facade/surface/aggregate_door.rb +42 -27
- data/lib/hecks/facade/surface/chapter.rb +26 -17
- data/lib/hecks/facade/surface.rb +16 -3
- data/lib/hecks/facade.rb +15 -4
- data/lib/hecks/forms/app.rb +46 -30
- data/lib/hecks/forms/command_form_renderer.rb +70 -9
- data/lib/hecks/forms/field_renderer.rb +142 -6
- data/lib/hecks/forms/field_shape.rb +180 -19
- data/lib/hecks/forms/html.rb +51 -7
- data/lib/hecks/forms/index_renderer.rb +14 -2
- data/lib/hecks/forms/params.rb +120 -23
- data/lib/hecks/forms/port_argument.rb +2 -2
- data/lib/hecks/forms/query_form_renderer.rb +2 -2
- data/lib/hecks/forms/record_renderer.rb +2 -2
- data/lib/hecks/forms/record_table.rb +1 -1
- data/lib/hecks/forms/value_object_shape.rb +3 -3
- data/lib/hecks/forms.rb +24 -4
- data/lib/hecks/fqn.rb +1 -1
- data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
- data/lib/hecks/framework.rb +48 -17
- data/lib/hecks/freezer.rb +11 -11
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +90 -90
- data/lib/hecks/fuzzing/combination_miner.rb +119 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +140 -45
- data/lib/hecks/fuzzing/coverage_campaign.rb +118 -0
- data/lib/hecks/fuzzing/differential.rb +158 -0
- data/lib/hecks/fuzzing/domain_generator.rb +694 -0
- data/lib/hecks/fuzzing/era_boundary.rb +37 -18
- data/lib/hecks/fuzzing/form_census.rb +35 -20
- data/lib/hecks/fuzzing/generated_domain_check.rb +95 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +6 -6
- data/lib/hecks/fuzzing/isolated_boot.rb +62 -62
- data/lib/hecks/fuzzing/nondeterministic.rb +67 -0
- data/lib/hecks/fuzzing/persistence_parity.rb +19 -21
- data/lib/hecks/fuzzing/properties/corrections.rb +9 -9
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +154 -49
- data/lib/hecks/fuzzing/properties/guards.rb +42 -42
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +32 -32
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +22 -35
- data/lib/hecks/fuzzing/properties/outbox.rb +24 -24
- data/lib/hecks/fuzzing/properties/querying.rb +17 -17
- data/lib/hecks/fuzzing/properties.rb +64 -40
- data/lib/hecks/fuzzing/qa_settings.rb +152 -0
- data/lib/hecks/fuzzing/replay.rb +114 -114
- data/lib/hecks/fuzzing/rotation_priority.rb +10 -10
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +113 -0
- data/lib/hecks/fuzzing/self_consistency.rb +141 -112
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +35 -35
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +58 -31
- data/lib/hecks/fuzzing/sequence_generator.rb +111 -23
- data/lib/hecks/fuzzing/shrinker.rb +197 -0
- data/lib/hecks/fuzzing/structural_skips.rb +23 -130
- data/lib/hecks/fuzzing/sweep_depth.rb +4 -4
- data/lib/hecks/fuzzing/target_capabilities.rb +49 -18
- data/lib/hecks/fuzzing/value_generator.rb +14 -14
- data/lib/hecks/fuzzing.rb +5 -0
- data/lib/hecks/grammar/evolve.rb +10 -10
- data/lib/hecks/grammar.rb +7 -7
- data/lib/hecks/ir.rb +13 -13
- data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
- data/lib/hecks/language/bluebook/policy.bluebook +11 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
- data/lib/hecks/language/oidc.json +5 -0
- data/lib/hecks/literal.rb +9 -9
- data/lib/hecks/naming.rb +25 -25
- data/lib/hecks/ports/access_control.rb +58 -2
- data/lib/hecks/ports/agent/answers.rb +83 -6
- data/lib/hecks/ports/agent.rb +119 -35
- data/lib/hecks/ports/authentication.rb +44 -4
- data/lib/hecks/ports/authorization.rb +53 -11
- data/lib/hecks/ports/clock.rb +42 -23
- data/lib/hecks/ports/extraction.rb +16 -0
- data/lib/hecks/ports/identity_assignment.rb +24 -2
- data/lib/hecks/ports/identity_generation.rb +17 -3
- data/lib/hecks/ports/identity_resolution.rb +18 -1
- data/lib/hecks/ports/loading.rb +4 -0
- data/lib/hecks/ports/persistence/append_only.rb +172 -8
- data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
- data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
- data/lib/hecks/ports/persistence/execution.rb +4 -0
- data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
- data/lib/hecks/ports/persistence/plugin.rb +42 -4
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
- data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +28 -5
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +57 -4
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
- data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +42 -8
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -19
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +5 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
- data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
- data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
- data/lib/hecks/ports/persistence/state_codec.rb +319 -0
- data/lib/hecks/ports/persistence.rb +36 -1
- data/lib/hecks/ports/projection.rb +61 -7
- data/lib/hecks/ports/query/in_memory.rb +3 -3
- data/lib/hecks/ports/query/ordering.rb +6 -6
- data/lib/hecks/ports/query.rb +35 -0
- data/lib/hecks/projections/bootstrap_table.rb +112 -0
- data/lib/hecks/projections/diagrams.rb +75 -75
- data/lib/hecks/projections/glossary/html.rb +3 -3
- data/lib/hecks/projections/glossary/markdown.rb +3 -3
- data/lib/hecks/projections/glossary/mermaid.rb +1 -1
- data/lib/hecks/projections/glossary/sections.rb +1 -1
- data/lib/hecks/projections/glossary/sentences.rb +5 -5
- data/lib/hecks/projections/glossary.rb +15 -15
- data/lib/hecks/projections/ir.rb +1 -1
- data/lib/hecks/projections/model/deviations.rb +18 -17
- data/lib/hecks/projections/model.rb +25 -21
- data/lib/hecks/projections/oidc.rb +7 -7
- data/lib/hecks/projections/parser_table.rb +5 -5
- data/lib/hecks/projections/reference.rb +3 -3
- data/lib/hecks/projections/rust_vocabulary.rb +443 -0
- data/lib/hecks/projections/shape.rb +2 -2
- data/lib/hecks/projections/statements.rb +11 -11
- data/lib/hecks/projections/vocabulary.rb +9 -9
- data/lib/hecks/projections.rb +5 -3
- data/lib/hecks/projector/cli_projector.rb +29 -29
- data/lib/hecks/projector/docs_projector.rb +13 -13
- data/lib/hecks/projector/exporter.rb +42 -21
- data/lib/hecks/projector/ir_projector.rb +1 -1
- data/lib/hecks/projector/narrate_projector.rb +11 -11
- data/lib/hecks/projector/target.rb +13 -13
- data/lib/hecks/projector.rb +15 -15
- data/lib/hecks/query_ir.rb +47 -47
- data/lib/hecks/query_specification/common/comparators.rb +19 -3
- data/lib/hecks/query_specification/common/comparison.rb +109 -27
- data/lib/hecks/query_specification/common/dsl.rb +65 -9
- data/lib/hecks/query_specification/common/null_policy.rb +57 -13
- data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
- data/lib/hecks/query_specification/common/options.rb +25 -0
- data/lib/hecks/query_specification/field_path.rb +69 -15
- data/lib/hecks/query_specification/hop_path.rb +57 -20
- data/lib/hecks/query_specification/read_model/specification.rb +4 -0
- data/lib/hecks/rendering.rb +3 -3
- data/lib/hecks/router/namespace_installer.rb +3 -3
- data/lib/hecks/router.rb +1 -1
- data/lib/hecks/runtime/aggregate_lock.rb +11 -11
- data/lib/hecks/runtime/boot_gates.rb +3 -3
- data/lib/hecks/runtime/caller.rb +8 -8
- data/lib/hecks/runtime/capability_graph.rb +2 -2
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +22 -25
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +42 -63
- data/lib/hecks/runtime/command_interpreter.rb +159 -102
- data/lib/hecks/runtime/command_rules/admissibility.rb +69 -69
- data/lib/hecks/runtime/command_rules/arithmetic.rb +137 -91
- data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
- data/lib/hecks/runtime/command_rules/emission.rb +18 -1
- data/lib/hecks/runtime/command_rules/references.rb +35 -35
- data/lib/hecks/runtime/command_rules.rb +3 -0
- data/lib/hecks/runtime/dependency_planning.rb +11 -11
- data/lib/hecks/runtime/dispatcher.rb +283 -90
- data/lib/hecks/runtime/entity_element.rb +151 -67
- data/lib/hecks/runtime/entity_interpreter.rb +108 -82
- data/lib/hecks/runtime/errors.rb +19 -19
- data/lib/hecks/runtime/event.rb +6 -6
- data/lib/hecks/runtime/identity.rb +22 -22
- data/lib/hecks/runtime/instance.rb +39 -14
- data/lib/hecks/runtime/interpreting.rb +12 -12
- data/lib/hecks/runtime/invocation.rb +276 -0
- data/lib/hecks/runtime/loader.rb +14 -14
- data/lib/hecks/runtime/outbox.rb +23 -23
- data/lib/hecks/runtime/policy_interpreter.rb +54 -54
- data/lib/hecks/runtime/port_operation_interpreter.rb +22 -19
- data/lib/hecks/runtime/query_interpreter.rb +55 -64
- data/lib/hecks/runtime/reaction_invocation.rb +20 -20
- data/lib/hecks/runtime/read_model_interpreter.rb +40 -40
- data/lib/hecks/runtime/rebuild_sweep.rb +4 -4
- data/lib/hecks/runtime/reference_hop.rb +6 -6
- data/lib/hecks/runtime/refusal_wording.rb +92 -115
- data/lib/hecks/runtime/registry/saga_persistence.rb +21 -21
- data/lib/hecks/runtime/registry/verification.rb +36 -26
- data/lib/hecks/runtime/registry.rb +45 -28
- data/lib/hecks/runtime/remote_dispatcher.rb +38 -23
- data/lib/hecks/runtime/routing.rb +10 -153
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +17 -17
- data/lib/hecks/runtime/saga_interpreter.rb +64 -64
- data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
- data/lib/hecks/runtime/tenant_check.rb +9 -9
- data/lib/hecks/runtime/tenant_scope.rb +5 -5
- data/lib/hecks/runtime/value/admission.rb +75 -30
- data/lib/hecks/runtime/value/coercion.rb +318 -127
- data/lib/hecks/runtime/value/entity_list_coercion.rb +34 -34
- data/lib/hecks/runtime/value.rb +21 -21
- data/lib/hecks/runtime.rb +7 -7
- data/lib/hecks/storehouse.rb +64 -64
- data/lib/hecks/version.rb +3 -3
- data/lib/hecks/vocabulary.rb +205 -4
- data/lib/hecks.rb +13 -11
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +11 -11
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +14 -12
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +11 -11
- metadata +22 -2
|
@@ -12,28 +12,32 @@ module Hecks
|
|
|
12
12
|
# the declared routing `version:` — is excluded, so editing behavior
|
|
13
13
|
# never bumps an era and editing shape always does.
|
|
14
14
|
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
15
|
+
# ## Constraints left out on purpose
|
|
16
|
+
#
|
|
17
|
+
# Three attribute facts on the wire are constraints on persisted
|
|
18
|
+
# values, not behavior, and are still excluded — each decided, not
|
|
17
19
|
# overlooked:
|
|
18
20
|
#
|
|
19
21
|
# `optional` — required-ness is enforced at dispatch; stored rows
|
|
20
22
|
# are never re-validated on read, so flipping it
|
|
21
23
|
# strands nothing already written. Excluded.
|
|
22
|
-
# `pattern` — same argument: a fact about what may be
|
|
24
|
+
# `pattern` — same argument: a fact about what may be written
|
|
23
25
|
# next, not about what was stored. Excluded.
|
|
24
26
|
# `admits` — the sharpest of the three: narrowing a closed set
|
|
25
|
-
#
|
|
26
|
-
# carries only the set's
|
|
27
|
+
# can strand stored rows outside it, and the wire
|
|
28
|
+
# carries only the set's name, so a set whose members
|
|
27
29
|
# changed under a stable name is invisible even to a
|
|
28
30
|
# projection that included the fact (the same lesson
|
|
29
31
|
# recursive value-object drift taught). Excluded, and
|
|
30
|
-
#
|
|
32
|
+
# named as a gap: constraint tightening has no
|
|
31
33
|
# translation-rule vocabulary to acknowledge it yet,
|
|
32
34
|
# so including it would mint era bumps nothing can
|
|
33
35
|
# explain. When the translation language grows a
|
|
34
36
|
# constraint-acknowledgment rule, `admits` (by member
|
|
35
37
|
# list, not by name) is first in line, and that
|
|
36
|
-
# change bumps FORM_VERSION
|
|
38
|
+
# change bumps `FORM_VERSION`.
|
|
39
|
+
#
|
|
40
|
+
# ## Structural comparison over the dump form
|
|
37
41
|
#
|
|
38
42
|
# Projection runs over the canonical dump form (`to_h`, JSON
|
|
39
43
|
# round-tripped), so a verdict depends only on the IR — never on live
|
|
@@ -41,6 +45,11 @@ module Hecks
|
|
|
41
45
|
module StorageShape
|
|
42
46
|
module_function
|
|
43
47
|
|
|
48
|
+
# Reduces a bluebook to the parts of its IR that decide what stored data looks like.
|
|
49
|
+
#
|
|
50
|
+
# @param bluebook [Bluebook::Chapter] the bluebook whose `to_h` dump is projected
|
|
51
|
+
# @return [Hash{String => Object}] `"name"` (the bluebook's name) and `"aggregates"`, an
|
|
52
|
+
# Array of the Hashes `project_aggregate` builds, sorted by aggregate name
|
|
44
53
|
def project(bluebook)
|
|
45
54
|
domain = JSON.parse(JSON.generate(bluebook.to_h))
|
|
46
55
|
{
|
|
@@ -51,30 +60,55 @@ module Hecks
|
|
|
51
60
|
}
|
|
52
61
|
end
|
|
53
62
|
|
|
63
|
+
# Compares two bluebooks by storage shape alone, ignoring every behavioral difference.
|
|
64
|
+
#
|
|
65
|
+
# @param held [Bluebook::Chapter] the bluebook a held era was minted from
|
|
66
|
+
# @param current [Bluebook::Chapter] the bluebook booting now
|
|
67
|
+
# @return [Boolean] true when both project to an equal structure
|
|
54
68
|
def same?(held, current) = project(held) == project(current)
|
|
55
69
|
|
|
56
|
-
# The canonical serialization the Ruby scaffold hashes at
|
|
70
|
+
# The canonical serialization the Ruby scaffold hashes at mint time
|
|
57
71
|
# — the one moment identity is computed. Nothing ever recomputes a
|
|
58
72
|
# stored era name to verify it, so this form can evolve freely.
|
|
73
|
+
#
|
|
74
|
+
# @param bluebook [Bluebook::Chapter] the bluebook to serialize
|
|
75
|
+
# @return [String] compact JSON text of `project(bluebook)`
|
|
59
76
|
def canonical(bluebook) = JSON.generate(project(bluebook))
|
|
60
77
|
|
|
78
|
+
# Computes the era identity of a bluebook's storage shape.
|
|
79
|
+
#
|
|
80
|
+
# @param bluebook [Bluebook::Chapter] the bluebook being minted
|
|
81
|
+
# @return [String] 64 lowercase hex characters, the SHA-256 of `canonical(bluebook)`
|
|
61
82
|
def mint_hash(bluebook) = Digest::SHA256.hexdigest(canonical(bluebook))
|
|
62
83
|
|
|
63
84
|
LABEL_LENGTH = 6
|
|
85
|
+
|
|
86
|
+
# Shortens the era identity to the label translation edges and refusals use.
|
|
87
|
+
#
|
|
88
|
+
# @param bluebook [Bluebook::Chapter] the bluebook being minted
|
|
89
|
+
# @return [String] the first `LABEL_LENGTH` hex characters of `mint_hash(bluebook)`
|
|
64
90
|
def mint_label(bluebook) = mint_hash(bluebook)[0, LABEL_LENGTH]
|
|
65
91
|
|
|
66
92
|
# The version of the canonical serialization above. Minted-once
|
|
67
93
|
# means a stored name stays valid across form changes — but only
|
|
68
|
-
# if each name records
|
|
94
|
+
# if each name records which form minted it, so v1-named and
|
|
69
95
|
# v2-named eras coexist legibly. Stored beside every minted hash
|
|
70
96
|
# (names.tsv fourth field / hecks_eras.canon_form); bump this in
|
|
71
97
|
# the same change that alters project/canonical output.
|
|
72
98
|
FORM_VERSION = 1
|
|
73
99
|
|
|
100
|
+
# Projects one dumped aggregate down to its identity, lifecycle field and attributes.
|
|
101
|
+
#
|
|
102
|
+
# @param aggregate [Hash{String => Object}] one entry of the dumped bluebook's
|
|
103
|
+
# `"aggregates"` list, string-keyed after the JSON round-trip
|
|
104
|
+
# @return [Hash{String => Object}] `"name"`, `"identity"` (Array<String> of declared
|
|
105
|
+
# identity paths, `[]` when none is declared), `"lifecycle_field"` (String, or nil
|
|
106
|
+
# without a lifecycle) and `"attributes"` (Array of `project_attribute` Hashes, sorted
|
|
107
|
+
# by name)
|
|
74
108
|
def project_aggregate(aggregate)
|
|
75
109
|
{
|
|
76
110
|
"name" => aggregate["name"],
|
|
77
|
-
# The declared identity paths,
|
|
111
|
+
# The declared identity paths, as a list, in declaration order —
|
|
78
112
|
# order is semantic (the paths join in order to form the id).
|
|
79
113
|
# No "id" fallback: an aggregate that declares nothing has [],
|
|
80
114
|
# and that is a real declared state, distinct from an aggregate
|
|
@@ -87,6 +121,16 @@ module Hecks
|
|
|
87
121
|
}
|
|
88
122
|
end
|
|
89
123
|
|
|
124
|
+
# Projects one dumped attribute to its name, cardinality and full type signature.
|
|
125
|
+
#
|
|
126
|
+
# @param aggregate [Hash{String => Object}] the dumped aggregate that owns the attribute,
|
|
127
|
+
# searched for the value objects and entities its type may name
|
|
128
|
+
# @param attribute [Hash{String => Object}] the dumped attribute, read for `"name"`,
|
|
129
|
+
# `"list"` and `"type"`
|
|
130
|
+
# @param seen [Array<String>] type names already being expanded, which stops a
|
|
131
|
+
# self-referencing type from recursing forever
|
|
132
|
+
# @return [Hash{String => Object}] `"name"` (String), `"list"` (Boolean) and `"type"`
|
|
133
|
+
# (whatever `type_signature` returns)
|
|
90
134
|
def project_attribute(aggregate, attribute, seen)
|
|
91
135
|
{
|
|
92
136
|
"name" => attribute["name"].to_s,
|
|
@@ -99,6 +143,14 @@ module Hecks
|
|
|
99
143
|
# members' full signatures for a value object or entity — so two
|
|
100
144
|
# attributes with the same declared type name but different
|
|
101
145
|
# internals are never mistaken for unchanged.
|
|
146
|
+
#
|
|
147
|
+
# @param aggregate [Hash{String => Object}] the dumped aggregate whose value objects and
|
|
148
|
+
# entities are searched for `type_name`
|
|
149
|
+
# @param type_name [String] the attribute's declared type name
|
|
150
|
+
# @param seen [Array<String>] type names already being expanded
|
|
151
|
+
# @return [String, Hash{String => Object}] `type_name` itself for a primitive or a type
|
|
152
|
+
# already in `seen`; otherwise `"type"` plus `"members"`, the member attributes'
|
|
153
|
+
# `project_attribute` Hashes sorted by name
|
|
102
154
|
def type_signature(aggregate, type_name, seen)
|
|
103
155
|
container = nested_type(aggregate, type_name)
|
|
104
156
|
return type_name if container.nil? || seen.include?(type_name)
|
|
@@ -111,6 +163,12 @@ module Hecks
|
|
|
111
163
|
}
|
|
112
164
|
end
|
|
113
165
|
|
|
166
|
+
# Looks a type name up among a dumped aggregate's value objects, then its entities.
|
|
167
|
+
#
|
|
168
|
+
# @param aggregate [Hash{String => Object}] the dumped aggregate to search
|
|
169
|
+
# @param type_name [String] the declared type name to find
|
|
170
|
+
# @return [Hash{String => Object}, nil] the dumped value object or entity; nil when the
|
|
171
|
+
# name is a primitive or belongs to nothing this aggregate declares
|
|
114
172
|
def nested_type(aggregate, type_name)
|
|
115
173
|
(aggregate["value_objects"] || []).find { |vo| vo["name"] == type_name } ||
|
|
116
174
|
(aggregate["entities"] || []).find { |entity| entity["name"] == type_name }
|
|
@@ -7,21 +7,27 @@ module Hecks
|
|
|
7
7
|
# ── the human gate, made real ──────────────────────────────────
|
|
8
8
|
#
|
|
9
9
|
# For the five portable rule kinds the mint verifies the edge
|
|
10
|
-
# mechanically (Layer 2
|
|
10
|
+
# mechanically (Layer 2 is the cross-execution equivalence gate).
|
|
11
11
|
# A compute has no mechanical verification — the Layer-3 sample a
|
|
12
12
|
# human reads is the only one there is — so a compute edge cannot
|
|
13
13
|
# mint until someone has run `bin/translation_audit … --approve`.
|
|
14
14
|
#
|
|
15
|
-
# The approval binds to
|
|
15
|
+
# The approval binds to what was actually reviewed, on both axes:
|
|
16
16
|
# a content digest of the parsed edge (change the edge's meaning
|
|
17
17
|
# and the approval lapses; a comment does not), and the journal's
|
|
18
|
-
# high-water ordinal at review time, recorded
|
|
18
|
+
# high-water ordinal at review time, recorded in the target
|
|
19
19
|
# database — a token in the repo could not bind to the data, and
|
|
20
20
|
# samples reviewed against staging or against production-as-of-T
|
|
21
21
|
# say nothing about a database that has moved on. Mint stays
|
|
22
22
|
# non-interactive and its lock stays short; the human decision
|
|
23
23
|
# happens in the audit tool, where the samples are.
|
|
24
24
|
module ApprovalDigest
|
|
25
|
+
# Fingerprints a parsed translation edge, so an approval lapses when the edge's
|
|
26
|
+
# meaning changes but not when only a comment does.
|
|
27
|
+
#
|
|
28
|
+
# @param edge [Bluebook::Translation] the parsed translation edge
|
|
29
|
+
# @return [String] 64 lowercase hex characters: the SHA-256 of the edge's exported
|
|
30
|
+
# JSON (`Projector::Exporter.translation_hash`)
|
|
25
31
|
def edge_digest(edge)
|
|
26
32
|
Digest::SHA256.hexdigest(JSON.generate(Projector::Exporter.translation_hash(edge)))
|
|
27
33
|
end
|
|
@@ -8,11 +8,18 @@ module Hecks
|
|
|
8
8
|
module Audit
|
|
9
9
|
# Layer 1 — from the bluebook alone: every translated state must
|
|
10
10
|
# pass the new era's types, value-object invariants, and lifecycle.
|
|
11
|
-
# This is also where a
|
|
11
|
+
# This is also where a new, stricter invariant that old records
|
|
12
12
|
# violate surfaces — there is no "grandfather old records"
|
|
13
13
|
# construct, and the remedy is relaxing the invariant or explicit
|
|
14
14
|
# remediation, never a translation rule.
|
|
15
15
|
module LayerOne
|
|
16
|
+
# Hydrates every translated state as a current-era instance and records each one
|
|
17
|
+
# the era's types, invariants or lifecycle refuse.
|
|
18
|
+
#
|
|
19
|
+
# @param violations [Array<String>] collector this method appends messages to
|
|
20
|
+
# @param aggregate [Bluebook::Aggregate] the current era's IR for the aggregate
|
|
21
|
+
# @param after [Hash{String => Hash}] translated state per record id, as parsed JSON
|
|
22
|
+
# @return [void]
|
|
16
23
|
def layer_one!(violations, aggregate, after)
|
|
17
24
|
after.each do |id, state|
|
|
18
25
|
symbolized = JSON.parse(JSON.generate(state), symbolize_names: true)
|
|
@@ -25,7 +32,7 @@ module Hecks
|
|
|
25
32
|
# legitimately declared just as a `from:` (a terminal
|
|
26
33
|
# transition's source, never anyone's target) is real and
|
|
27
34
|
# reachable but invisible to it. `ModelCheck.full_states`
|
|
28
|
-
# is the full declared set (default, every target,
|
|
35
|
+
# is the full declared set (default, every target, and
|
|
29
36
|
# every from) that `fuzzing/properties.rb`'s own replay
|
|
30
37
|
# check already uses for this identical question — see its
|
|
31
38
|
# comment on this same hole.
|
|
@@ -7,8 +7,11 @@ module Hecks
|
|
|
7
7
|
# Layer 2 — from the edge alone: per-rule value preservation, no
|
|
8
8
|
# leftover source keys, and id-set conservation across the edge.
|
|
9
9
|
module LayerTwo
|
|
10
|
+
# Checks one aggregate's records across the edge for id conservation and, where a
|
|
11
|
+
# per-record comparison is possible, agreement with the reference transform.
|
|
12
|
+
#
|
|
10
13
|
# Per-rule value preservation and leftover source keys are checked
|
|
11
|
-
# against the reference transform
|
|
14
|
+
# against the reference transform in full — never rule by rule in
|
|
12
15
|
# isolation, because rules interact (a rename whose value a later
|
|
13
16
|
# move partially consumes preserves exactly what the transform
|
|
14
17
|
# says it preserves, no more). This makes every mint a run of the
|
|
@@ -17,10 +20,20 @@ module Hecks
|
|
|
17
20
|
# transform produces `expected`; they must agree byte-for-byte on
|
|
18
21
|
# every path a compute doesn't own. Compute paths are exempt — the
|
|
19
22
|
# SQL is their only implementation, and the Layer-3 sample is
|
|
20
|
-
# their only review. A rekeyed aggregate is exempt from this
|
|
23
|
+
# their only review. A rekeyed aggregate is exempt from this whole
|
|
21
24
|
# per-record check, for the same reason and one more: there is no
|
|
22
25
|
# old-id → new-id correspondence to look `after` up by once the id
|
|
23
26
|
# itself is what changed.
|
|
27
|
+
#
|
|
28
|
+
# @param violations [Array<String>] collector this method appends messages to
|
|
29
|
+
# @param aggregate [Bluebook::Aggregate] the current era's IR for the aggregate
|
|
30
|
+
# @param declared [Bluebook::TranslationAggregate, nil] this edge's rules for the
|
|
31
|
+
# aggregate; nil limits the check to id conservation
|
|
32
|
+
# @param before [Hash{String => Hash}] source state per record id, as parsed JSON
|
|
33
|
+
# @param after [Hash{String => Hash}] translated state per record id, as parsed JSON
|
|
34
|
+
# @return [void]
|
|
35
|
+
# @raise [Runtime::WiringError] if the reference transform cannot translate a `before`
|
|
36
|
+
# state: a convert meets an unmapped value, or a move nests under a non-Hash
|
|
24
37
|
def layer_two!(violations, aggregate, declared, before, after)
|
|
25
38
|
rekeyed = declared && !declared.rekeys.empty?
|
|
26
39
|
|
|
@@ -38,15 +51,25 @@ module Hecks
|
|
|
38
51
|
check_value_preservation!(violations, aggregate, declared, before, after)
|
|
39
52
|
end
|
|
40
53
|
|
|
54
|
+
# Records a violation when the edge loses, gains or collides record ids.
|
|
55
|
+
#
|
|
41
56
|
# Record-count / id-set conservation — one of the two properties
|
|
42
57
|
# this module's own header names, and independent of the other
|
|
43
58
|
# (per-rule value preservation, below): it needs only `before`,
|
|
44
59
|
# `after`, and whether the edge rekeyed, never the declared rules
|
|
45
60
|
# themselves.
|
|
61
|
+
#
|
|
62
|
+
# @param violations [Array<String>] collector this method appends at most one message to
|
|
63
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate, named in the message
|
|
64
|
+
# @param before [Hash{String => Hash}] source state per record id
|
|
65
|
+
# @param after [Hash{String => Hash}] translated state per record id
|
|
66
|
+
# @param rekeyed [Boolean, nil] truthy when the edge rekeys, which compares record
|
|
67
|
+
# counts instead of id sets
|
|
68
|
+
# @return [void]
|
|
46
69
|
def check_id_conservation!(violations, aggregate, before, after, rekeyed:)
|
|
47
|
-
# A rekey legitimately changes the id
|
|
70
|
+
# A rekey legitimately changes the id set (that's the entire
|
|
48
71
|
# point) — set-equality would flag every honest rekey as data
|
|
49
|
-
# loss. What must still hold is
|
|
72
|
+
# loss. What must still hold is record count: a botched rekey
|
|
50
73
|
# colliding two distinct old ids onto one new id, or dropping one
|
|
51
74
|
# (its SQL returning NULL), shows up as the count going down —
|
|
52
75
|
# caught here without needing to track the old→new mapping
|
|
@@ -87,21 +110,32 @@ module Hecks
|
|
|
87
110
|
end
|
|
88
111
|
end
|
|
89
112
|
|
|
113
|
+
# Round-trips state through JSON so both sides of the comparison share one spelling.
|
|
114
|
+
#
|
|
115
|
+
# @param state [Hash, nil] a state with Symbol or String keys
|
|
116
|
+
# @return [Hash{String => Object}, nil] a deep copy with String keys at every depth
|
|
90
117
|
def normalize(state) = JSON.parse(JSON.generate(state))
|
|
91
118
|
|
|
119
|
+
# Removes the paths a compute owns from a normalized state, in place.
|
|
120
|
+
#
|
|
92
121
|
# Exempts exactly the paths a compute owns, not the whole
|
|
93
122
|
# top-level attribute it happens to live under. A bare path
|
|
94
123
|
# ("price_cents") is itself the compute's entire value — dropping
|
|
95
124
|
# the whole top-level key is correct, there's nothing else there
|
|
96
|
-
# to check. A
|
|
125
|
+
# to check. A dotted path ("price.cents") only owns that one
|
|
97
126
|
# member of the value object it reaches into; every sibling
|
|
98
127
|
# member (e.g. "price.currency") is untouched by the compute and
|
|
99
128
|
# must stay subject to the equivalence check below. Blanket-
|
|
100
|
-
# dropping the whole top-level key for a dotted compute
|
|
129
|
+
# dropping the whole top-level key for a dotted compute would
|
|
101
130
|
# exempt the entire attribute — silent data loss elsewhere in the
|
|
102
131
|
# same value object (a migration that nulls or drops a sibling
|
|
103
|
-
# field)
|
|
104
|
-
# entire purpose is to catch exactly that.
|
|
132
|
+
# field) would produce zero violations, defeating the one gate
|
|
133
|
+
# whose entire purpose is to catch exactly that.
|
|
134
|
+
#
|
|
135
|
+
# @param state [Hash{String => Object}] a normalized state; mutated
|
|
136
|
+
# @param paths [Array<String>] bare (`"price_cents"`) or dotted (`"price.cents"`)
|
|
137
|
+
# compute paths
|
|
138
|
+
# @return [Hash{String => Object}] `state` itself, with those paths deleted
|
|
105
139
|
def strip_compute_paths(state, paths)
|
|
106
140
|
paths.each do |path|
|
|
107
141
|
segments = path.split(".")
|
|
@@ -4,9 +4,11 @@ module Hecks
|
|
|
4
4
|
# New-era attributes that nothing feeds: absent from every
|
|
5
5
|
# translated state, produced by no rule, and carrying no default.
|
|
6
6
|
# A report, not a violation — the remedy is a `default:` on the
|
|
7
|
-
# attribute, and the loud refusal for a
|
|
7
|
+
# attribute, and the loud refusal for a required one comes from
|
|
8
8
|
# Layer 1 the moment an invariant reads it.
|
|
9
9
|
module UnfedReport
|
|
10
|
+
# Lists the current era's attributes that no rule, default or translated record feeds.
|
|
11
|
+
#
|
|
10
12
|
# `fed` build-up plus one ordered guard chain per attribute
|
|
11
13
|
# (already fed, has a default, actually present in some record, no
|
|
12
14
|
# records to check at all) answering a single question — "is
|
|
@@ -14,6 +16,13 @@ module Hecks
|
|
|
14
16
|
# rules out one way the answer is "not unfed after all" before the
|
|
15
17
|
# next is even worth asking. Splitting the guards out would just
|
|
16
18
|
# turn each into a same-shaped one-line predicate call.
|
|
19
|
+
#
|
|
20
|
+
# @param aggregate [Bluebook::Aggregate] the current era's IR for the aggregate
|
|
21
|
+
# @param declared [Bluebook::TranslationAggregate, nil] this edge's rules; the
|
|
22
|
+
# destinations of its renames, moves, converts and computes count as fed
|
|
23
|
+
# @param after [Hash{String => Hash}] translated state per record id
|
|
24
|
+
# @return [Array<String>] names of unfed attributes, in declaration order; `[]` when
|
|
25
|
+
# every attribute is fed or `after` holds no records
|
|
17
26
|
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
18
27
|
# rubocop:disable-next Metrics/PerceivedComplexity
|
|
19
28
|
def unfed(aggregate, declared, after)
|
|
@@ -36,6 +45,12 @@ module Hecks
|
|
|
36
45
|
end
|
|
37
46
|
end
|
|
38
47
|
|
|
48
|
+
# Reads a dotted path out of a state whose keys may be Strings or Symbols.
|
|
49
|
+
#
|
|
50
|
+
# @param state [Hash, nil] the state to read
|
|
51
|
+
# @param path [String, Symbol] a bare or dotted path, such as `"price.cents"`
|
|
52
|
+
# @return [Object, nil] the value held at the path, `false` included; nil when `state`
|
|
53
|
+
# is nil, a segment is absent, or a segment's parent is not a Hash
|
|
39
54
|
def dig_path(state, path)
|
|
40
55
|
return nil if state.nil?
|
|
41
56
|
|
|
@@ -12,10 +12,14 @@ module Hecks
|
|
|
12
12
|
# rules), audit/layer_two.rb (the edge against the reference
|
|
13
13
|
# transform), audit/unfed_report.rb (what nothing feeds), and
|
|
14
14
|
# audit/approval_digest.rb (the human gate's binding). Layer 3 — the
|
|
15
|
-
# before/after sample a
|
|
15
|
+
# before/after sample a human approves, intent not being derivable —
|
|
16
16
|
# is assembled right here in `check`.
|
|
17
17
|
module Audit
|
|
18
18
|
Verdict = Struct.new(:violations, :dropped, :unfed, :samples, keyword_init: true) do
|
|
19
|
+
# Reports whether the mechanical layers found nothing to refuse.
|
|
20
|
+
#
|
|
21
|
+
# @return [Boolean] true when `violations` is empty; `dropped`, `unfed` and `samples`
|
|
22
|
+
# are reports and never affect the answer
|
|
19
23
|
def ok? = violations.empty?
|
|
20
24
|
end
|
|
21
25
|
|
|
@@ -28,9 +32,20 @@ module Hecks
|
|
|
28
32
|
|
|
29
33
|
module_function
|
|
30
34
|
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
35
|
+
# Audits one aggregate's records across one translation edge and gathers what a
|
|
36
|
+
# human reviewer needs to see.
|
|
37
|
+
#
|
|
38
|
+
# @param aggregate [Bluebook::Aggregate] the current era's IR for the aggregate
|
|
39
|
+
# @param declared [Bluebook::TranslationAggregate, nil] this edge's rules for the
|
|
40
|
+
# aggregate; nil when the edge declares none
|
|
41
|
+
# @param before [Hash{String => Hash}] the source era's latest state per record id, as
|
|
42
|
+
# parsed JSON
|
|
43
|
+
# @param after [Hash{String => Hash}] the translated state per record id, as parsed JSON
|
|
44
|
+
# @return [Translation::Audit::Verdict] `violations` (Layer 1 and 2 messages), `dropped`
|
|
45
|
+
# (declared drop paths as Strings), `unfed` (attribute names nothing feeds) and
|
|
46
|
+
# `samples` (the Layer 3 before/after pairs)
|
|
47
|
+
# @raise [Runtime::WiringError] if Layer 2's reference transform cannot translate a
|
|
48
|
+
# `before` state: a convert meets an unmapped value, or a move nests under a non-Hash
|
|
34
49
|
def check(aggregate:, declared:, before:, after:)
|
|
35
50
|
violations = []
|
|
36
51
|
|
|
@@ -45,22 +60,38 @@ module Hecks
|
|
|
45
60
|
)
|
|
46
61
|
end
|
|
47
62
|
|
|
63
|
+
# Picks the first `SAMPLE_SIZE` records, by sorted id, for a human to compare.
|
|
64
|
+
#
|
|
65
|
+
# @param declared [Bluebook::TranslationAggregate, nil] this edge's rules; a rekey among
|
|
66
|
+
# them switches to `rekeyed_samples`
|
|
67
|
+
# @param before [Hash{String => Hash}] source state per record id
|
|
68
|
+
# @param after [Hash{String => Hash}] translated state per record id
|
|
69
|
+
# @return [Array<Hash{Symbol => Object}>] Hashes with `:id`, `:before` and `:after`;
|
|
70
|
+
# `:after` is nil for an id missing from `after`
|
|
48
71
|
def samples_for(declared, before, after)
|
|
49
72
|
return rekeyed_samples(before, after) if declared && !declared.rekeys.empty?
|
|
50
73
|
|
|
51
74
|
before.keys.sort.first(SAMPLE_SIZE).map { |id| { id: id, before: before[id], after: after[id] } }
|
|
52
75
|
end
|
|
53
76
|
|
|
77
|
+
# Samples both sides of a rekeying edge separately instead of pairing them by id.
|
|
78
|
+
#
|
|
54
79
|
# A rekey changes the id itself, so `before`'s and `after`'s
|
|
55
80
|
# keyspaces share nothing — pairing by matching id (the ordinary
|
|
56
81
|
# path above) would show every `after` as nil, telling a human
|
|
57
82
|
# nothing. Nothing in-process can compute the old→new
|
|
58
83
|
# correspondence either (the rekey's SQL is its only
|
|
59
84
|
# implementation, same as compute's own). Shown side by side
|
|
60
|
-
# instead, each labelled by its
|
|
85
|
+
# instead, each labelled by its own id: real records going in,
|
|
61
86
|
# real records coming out — not claimed to correspond one-to-one,
|
|
62
87
|
# but enough for the human this rule's only verification depends
|
|
63
88
|
# on to actually see real shapes and values, not a wall of null.
|
|
89
|
+
#
|
|
90
|
+
# @param before [Hash{String => Hash}] source state per old record id
|
|
91
|
+
# @param after [Hash{String => Hash}] translated state per new record id
|
|
92
|
+
# @return [Array<Hash{Symbol => Object}>] up to `SAMPLE_SIZE` Hashes whose `:id` ends in
|
|
93
|
+
# `" (before)"` with `:after` nil, then up to `SAMPLE_SIZE` ending in `" (after)"` with
|
|
94
|
+
# `:before` nil
|
|
64
95
|
def rekeyed_samples(before, after)
|
|
65
96
|
before.keys.sort.first(SAMPLE_SIZE).map { |id| { id: "#{id} (before)", before: before[id], after: nil } } +
|
|
66
97
|
after.keys.sort.first(SAMPLE_SIZE).map { |id| { id: "#{id} (after)", before: nil, after: after[id] } }
|
|
@@ -6,19 +6,33 @@ require_relative "../storage_shape"
|
|
|
6
6
|
module Hecks
|
|
7
7
|
module Translation
|
|
8
8
|
# The question re-attestation must answer before anything else: did
|
|
9
|
-
# the edit change the era's
|
|
9
|
+
# the edit change the era's shape, or only its text? Cosmetic edits
|
|
10
10
|
# (comments, whitespace, behavior) re-freeze safely; a shape change
|
|
11
11
|
# would retroactively redefine what era N *meant* for data already
|
|
12
12
|
# written under it, and refuses hard — there is no --accept past
|
|
13
13
|
# this guard.
|
|
14
14
|
#
|
|
15
15
|
# This does not violate minted-once. That prohibition exists so
|
|
16
|
-
# boot-time
|
|
16
|
+
# boot-time recognition never depends on canonicalization stability;
|
|
17
17
|
# this is operator-initiated repair, where a false negative is a
|
|
18
18
|
# loud refusal, never a silent misread.
|
|
19
19
|
module Reattest
|
|
20
20
|
module_function
|
|
21
21
|
|
|
22
|
+
# Decides whether an edited era text still projects to the shape the era was frozen
|
|
23
|
+
# with, refusing the edit when it does not.
|
|
24
|
+
#
|
|
25
|
+
# @param domain [String] the domain's name, used in refusal messages
|
|
26
|
+
# @param ordinal [Integer] the era's ordinal, used in refusal messages
|
|
27
|
+
# @param text [String] the held era's bluebook source as it now stands
|
|
28
|
+
# @param stored_hash [String, nil] the era's minted shape hash (SHA-256 hex); nil for an
|
|
29
|
+
# era that was never named
|
|
30
|
+
# @param stored_projection [Hash{String => Object}, nil] the era's stored
|
|
31
|
+
# `Runtime::StorageShape.project` result as parsed JSON; nil for a store without one
|
|
32
|
+
# @return [Symbol] `:cosmetic` when the shape is unchanged, `:unnamed` when neither a
|
|
33
|
+
# projection nor a hash is stored so no comparison is possible
|
|
34
|
+
# @raise [Runtime::WiringError] if `text` does not load as a bluebook, or it projects to
|
|
35
|
+
# a shape other than the stored one
|
|
22
36
|
def shape_guard!(domain:, ordinal:, text:, stored_hash:, stored_projection: nil)
|
|
23
37
|
bluebook = shadow(text)
|
|
24
38
|
unless bluebook
|
|
@@ -27,7 +41,7 @@ module Hecks
|
|
|
27
41
|
"bluebook — a held era text is bootable source; restore a loadable text"
|
|
28
42
|
end
|
|
29
43
|
|
|
30
|
-
# The stored
|
|
44
|
+
# The stored projection is the preferred comparison: structural,
|
|
31
45
|
# version-free, and the same mechanism every boot trusts — so
|
|
32
46
|
# this guard never depends on canonicalization stability, and a
|
|
33
47
|
# future canonical-form version cannot make cosmetic edits to
|
|
@@ -57,6 +71,12 @@ module Hecks
|
|
|
57
71
|
"under it; restore a text with the original shape"
|
|
58
72
|
end
|
|
59
73
|
|
|
74
|
+
# Parses bluebook source in a scratch registry, through a temporary file that is
|
|
75
|
+
# removed afterwards, without touching the live registry.
|
|
76
|
+
#
|
|
77
|
+
# @param source [String] bluebook source text
|
|
78
|
+
# @return [Bluebook::Chapter, nil] the first bluebook the source declares; nil when it
|
|
79
|
+
# declares none or fails to parse or load for any reason
|
|
60
80
|
def shadow(source)
|
|
61
81
|
file = Tempfile.new(["hecks-reattest-", ".bluebook"])
|
|
62
82
|
file.write(source)
|
|
@@ -2,23 +2,22 @@ require "json"
|
|
|
2
2
|
|
|
3
3
|
module Hecks
|
|
4
4
|
module Translation
|
|
5
|
-
# The closed,
|
|
6
|
-
# (
|
|
7
|
-
# that turns one TranslationAggregate's declared rules into
|
|
8
|
-
# jsonb-transforming SQL expression. No database connection, no
|
|
9
|
-
# watermark, no era chain, no catalog lookup: those
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
# itself.
|
|
5
|
+
# The closed, pure half of Postgres's own SQL compiler
|
|
6
|
+
# (ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb)
|
|
7
|
+
# — the part that turns one TranslationAggregate's declared rules into
|
|
8
|
+
# a jsonb-transforming SQL expression. No database connection, no
|
|
9
|
+
# watermark, no era chain, no catalog lookup: those live in
|
|
10
|
+
# head_compiler.rb's own per-mint assembly, which calls into this
|
|
11
|
+
# module instead of defining these methods itself.
|
|
13
12
|
#
|
|
14
|
-
#
|
|
15
|
-
# Adapters::PostgresEra::Lineage — so a
|
|
13
|
+
# A module of its own — not private methods on
|
|
14
|
+
# Adapters::PostgresEra::Lineage — so a second, adapter-agnostic
|
|
16
15
|
# caller (Exporter.translation_aggregate's build-time SQL export,
|
|
17
16
|
# feeding rust/host's own future boot-time mint) can call the exact
|
|
18
|
-
#
|
|
19
|
-
# that could silently drift the way
|
|
20
|
-
#
|
|
21
|
-
#
|
|
17
|
+
# same code Ruby's own mint path runs, not a hand-ported duplicate
|
|
18
|
+
# that could silently drift the way a hand-kept
|
|
19
|
+
# `Exporter.translation_hash` can drift from `hecks_eras`/
|
|
20
|
+
# `hecks_approvals`' real schema (leaving out rekeys and backfills).
|
|
22
21
|
module RuleCompiler
|
|
23
22
|
module_function
|
|
24
23
|
|
|
@@ -51,7 +50,7 @@ module Hecks
|
|
|
51
50
|
expression
|
|
52
51
|
end
|
|
53
52
|
|
|
54
|
-
# Whether
|
|
53
|
+
# Whether this edge's declared rules for this aggregate include a
|
|
55
54
|
# rekey — checked directly off the raw IR object, the same way
|
|
56
55
|
# every other rule kind is already read in `compile_rules`
|
|
57
56
|
# (`declared.computes`, `declared.moves`, ...), not through the
|
|
@@ -60,16 +59,16 @@ module Hecks
|
|
|
60
59
|
# module builds SQL straight off the IR either way.
|
|
61
60
|
def rekeyed?(declared) = declared && !declared.rekeys.empty?
|
|
62
61
|
|
|
63
|
-
#
|
|
62
|
+
# The only two places `aggregate_id` needs to change — guarded so
|
|
64
63
|
# the generated SQL for the overwhelming common case (no rekey
|
|
65
64
|
# declared) stays the bare `aggregate_id` passthrough it always
|
|
66
|
-
# was — this
|
|
65
|
+
# was — this case only appears in an edge that actually declares
|
|
67
66
|
# one.
|
|
68
67
|
def id_case(guard, declared)
|
|
69
68
|
"CASE WHEN #{guard} THEN #{compile_id_expression(declared)} ELSE aggregate_id END AS aggregate_id"
|
|
70
69
|
end
|
|
71
70
|
|
|
72
|
-
#
|
|
71
|
+
# The rekey's own SQL — reading `state` directly, not the
|
|
73
72
|
# progressively-built `expression` chain `compile_compute` reads
|
|
74
73
|
# from. A rekey doesn't consume or move any field the way a move
|
|
75
74
|
# or compute does, so there is no same-edge rename/move ordering
|
|
@@ -96,7 +95,7 @@ module Hecks
|
|
|
96
95
|
"LATERAL (SELECT (__s ->> #{text_literal(from)}) AS #{quote(from)}) __fields)"
|
|
97
96
|
end
|
|
98
97
|
|
|
99
|
-
# `PG::Connection.quote_ident` needs the `pg` gem
|
|
98
|
+
# `PG::Connection.quote_ident` needs the `pg` gem loaded, not
|
|
100
99
|
# connected — required here, lazily, the same "a domain that
|
|
101
100
|
# never wires PostgresEra should never need the gem" reasoning
|
|
102
101
|
# `PostgresEra.connect_for`'s own `require "pg"` already holds
|
|
@@ -32,7 +32,7 @@ module Hecks
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
# A vanished aggregate whose full shape reappears under exactly one
|
|
35
|
-
# new name was renamed. `retired` is only confident when
|
|
35
|
+
# new name was renamed. `retired` is only confident when nothing
|
|
36
36
|
# remains it could plausibly have become — a vanished aggregate
|
|
37
37
|
# beside an unmatched new one might be a rename-plus-reshape, and
|
|
38
38
|
# writing `retired` there would be a guess that strands data.
|
|
@@ -61,7 +61,7 @@ module Hecks
|
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
# Every changed path in one aggregate, resolved into rules by
|
|
64
|
-
# signature matching over the
|
|
64
|
+
# signature matching over the full path set — top-level names and
|
|
65
65
|
# dotted members alike. Unique signature pair: a rename (both
|
|
66
66
|
# top-level) or a move (any dotted end). Same path, same members,
|
|
67
67
|
# new type name: a retype. Anything else: unresolved, carrying its
|
|
@@ -81,7 +81,7 @@ module Hecks
|
|
|
81
81
|
appeared = appeared_paths(held_attrs, current_attrs, retyped)
|
|
82
82
|
|
|
83
83
|
# Mutates `rules` in place (not build-and-concat, like the two
|
|
84
|
-
# passes above) because it must
|
|
84
|
+
# passes above) because it must read its own earlier writes:
|
|
85
85
|
# `taken`, below, is recomputed from `rules` at the top of every
|
|
86
86
|
# iteration, so a target this same loop already claimed for an
|
|
87
87
|
# earlier vanished path is excluded from a later one. Passing a
|
|
@@ -92,7 +92,7 @@ module Hecks
|
|
|
92
92
|
end
|
|
93
93
|
|
|
94
94
|
# retype pass: same attribute name, same member structure, the
|
|
95
|
-
#
|
|
95
|
+
# type's own name changed
|
|
96
96
|
def retype_rules(held_attrs, current_attrs)
|
|
97
97
|
(held_attrs.keys & current_attrs.keys).filter_map do |name|
|
|
98
98
|
held = held_attrs[name]
|
|
@@ -166,7 +166,7 @@ module Hecks
|
|
|
166
166
|
paths
|
|
167
167
|
end
|
|
168
168
|
|
|
169
|
-
#
|
|
169
|
+
# The one thing `attribute_rules` could not see before — the shape
|
|
170
170
|
# projection already carries `"identity"` (`StorageShape
|
|
171
171
|
# .project_aggregate`), it was simply never read here. An
|
|
172
172
|
# unresolved placeholder, not a guess: `coverage_check.rb`'s own
|
|
@@ -8,7 +8,7 @@ module Hecks
|
|
|
8
8
|
module Writer
|
|
9
9
|
# The edge file, regenerated in place when one for the same shape
|
|
10
10
|
# pair already exists (matched textually — an unresolved file
|
|
11
|
-
# cannot be
|
|
11
|
+
# cannot be loaded to ask, that being the whole point of
|
|
12
12
|
# unresolved).
|
|
13
13
|
def write!(directory, edge)
|
|
14
14
|
translations_dir = File.join(directory, "translations")
|
|
@@ -6,13 +6,13 @@ module Hecks
|
|
|
6
6
|
module Translation
|
|
7
7
|
# The scaffold writes translations; humans resolve ambiguity. It
|
|
8
8
|
# diffs the held era's storage-shape projection against the current
|
|
9
|
-
# one (scaffold/differ.rb) and
|
|
9
|
+
# one (scaffold/differ.rb) and writes the edge file
|
|
10
10
|
# (scaffold/renderer.rb, scaffold/writer.rb): confident rules inline
|
|
11
11
|
# (unique signature pairs → rename/move, aggregate renames → `was:`,
|
|
12
12
|
# type renames with identical members → retype, aggregates gone
|
|
13
13
|
# without a successor → retired), ambiguities as parse-refusing
|
|
14
14
|
# `unresolved` constructs — never comments, so an unresolved file can
|
|
15
|
-
# only boot into a refusal, never a guess. It
|
|
15
|
+
# only boot into a refusal, never a guess. It never proposes a
|
|
16
16
|
# `compute` (a computation takes domain judgment no mechanical diff
|
|
17
17
|
# can infer) — an empty-candidate `unresolved` is what points the
|
|
18
18
|
# author there. It never proposes a `drop` either: data loss is a
|