hecks 1.3.0 → 1.5.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 +72 -7
- data/lib/hecks/adapters/driven/d1.rb +187 -23
- data/lib/hecks/adapters/driven/folder.rb +83 -10
- data/lib/hecks/adapters/driven/google_authentication.rb +33 -12
- data/lib/hecks/adapters/driven/governance_authorization.rb +87 -18
- data/lib/hecks/adapters/driven/heki/journal.rb +13 -4
- data/lib/hecks/adapters/driven/heki/saga_store.rb +56 -10
- data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
- data/lib/hecks/adapters/driven/heki.rb +106 -9
- data/lib/hecks/adapters/driven/identity_registry.rb +12 -2
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +25 -3
- data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
- data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
- data/lib/hecks/adapters/driven/lambda/client.rb +67 -14
- data/lib/hecks/adapters/driven/lambda.rb +82 -35
- data/lib/hecks/adapters/driven/local_storage.rb +83 -10
- data/lib/hecks/adapters/driven/memory.rb +205 -9
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +21 -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 +50 -11
- data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
- 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/system_clock.rb +3 -0
- data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
- data/lib/hecks/adapters/driven.rb +6 -4
- data/lib/hecks/adapters/driving/github_webhook.rb +31 -18
- data/lib/hecks/behaviors/dsl.rb +60 -2
- data/lib/hecks/behaviors/expectations.rb +190 -29
- data/lib/hecks/behaviors/ir.rb +12 -1
- data/lib/hecks/behaviors/rspec.rb +9 -1
- data/lib/hecks/behaviors/runner.rb +21 -2
- data/lib/hecks/behaviors.rb +9 -1
- data/lib/hecks/bluebook/aggregate.rb +43 -13
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +17 -10
- data/lib/hecks/bluebook/assembly/build.rb +26 -5
- data/lib/hecks/bluebook/assembly/contract.rb +98 -23
- data/lib/hecks/bluebook/assembly/contracts.rb +59 -52
- data/lib/hecks/bluebook/assembly/marks.rb +159 -30
- data/lib/hecks/bluebook/assembly/specializer.rb +38 -21
- data/lib/hecks/bluebook/assembly.rb +32 -14
- data/lib/hecks/bluebook/attribute.rb +26 -12
- data/lib/hecks/bluebook/behaviour/aggregate.rb +43 -11
- data/lib/hecks/bluebook/behaviour/attribute.rb +18 -5
- data/lib/hecks/bluebook/behaviour/chapter.rb +76 -5
- data/lib/hecks/bluebook/behaviour/command.rb +55 -25
- data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -7
- data/lib/hecks/bluebook/behaviour/entity.rb +20 -8
- data/lib/hecks/bluebook/behaviour/hexagon.rb +30 -4
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +27 -6
- data/lib/hecks/bluebook/behaviour/policy.rb +42 -17
- data/lib/hecks/bluebook/behaviour/process_manager.rb +39 -8
- data/lib/hecks/bluebook/behaviour/query.rb +6 -1
- data/lib/hecks/bluebook/behaviour/read_model.rb +29 -8
- data/lib/hecks/bluebook/behaviour/traits.rb +48 -12
- data/lib/hecks/bluebook/behaviour/value_object.rb +21 -9
- data/lib/hecks/bluebook/capabilities.rb +27 -0
- data/lib/hecks/bluebook/chapter.rb +51 -9
- data/lib/hecks/bluebook/command.rb +62 -17
- data/lib/hecks/bluebook/domain_port.rb +34 -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 +282 -123
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +112 -75
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +81 -2
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +486 -117
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +179 -47
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +284 -122
- 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 +191 -61
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +130 -30
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +38 -21
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
- data/lib/hecks/bluebook/dsl/policy_builder.rb +86 -36
- 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 +111 -47
- data/lib/hecks/bluebook/dsl/query_builder.rb +37 -8
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +127 -52
- data/lib/hecks/bluebook/dsl/rule_reference.rb +97 -43
- data/lib/hecks/bluebook/dsl/translation_builder.rb +150 -44
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +68 -20
- 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 +40 -11
- data/lib/hecks/bluebook/expression/ast_json.rb +128 -36
- data/lib/hecks/bluebook/expression/ast_reader.rb +32 -3
- data/lib/hecks/bluebook/expression/canonical_form.rb +55 -16
- data/lib/hecks/bluebook/expression/evaluator.rb +221 -43
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +54 -18
- data/lib/hecks/bluebook/expression/resolver.rb +369 -128
- data/lib/hecks/bluebook/hexagon.rb +35 -1
- data/lib/hecks/bluebook/lifecycle.rb +12 -1
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +2 -1
- data/lib/hecks/bluebook/meta_validator/judge.rb +126 -108
- data/lib/hecks/bluebook/meta_validator/plan.rb +81 -46
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +3 -2
- data/lib/hecks/bluebook/meta_validator/readings.rb +200 -50
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +68 -41
- data/lib/hecks/bluebook/meta_validator/shapes.rb +166 -21
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +286 -46
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +11 -10
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -5
- data/lib/hecks/bluebook/meta_validator.rb +235 -139
- data/lib/hecks/bluebook/model_check.rb +434 -104
- data/lib/hecks/bluebook/pattern_subset.rb +32 -10
- data/lib/hecks/bluebook/policy.rb +15 -13
- data/lib/hecks/bluebook/process_manager.rb +27 -14
- data/lib/hecks/bluebook/project_discovery.rb +5 -0
- data/lib/hecks/bluebook/project_loader.rb +40 -0
- data/lib/hecks/bluebook/project_register.rb +50 -6
- data/lib/hecks/bluebook/query.rb +31 -4
- data/lib/hecks/bluebook/read_model.rb +35 -15
- data/lib/hecks/bluebook/reference.rb +26 -13
- data/lib/hecks/bluebook/smoke_test.rb +46 -23
- data/lib/hecks/bluebook/synthesizer.rb +46 -12
- data/lib/hecks/bluebook/translation.rb +34 -5
- data/lib/hecks/bluebook/value_object.rb +29 -11
- data/lib/hecks/bluebook.rb +5 -6
- data/lib/hecks/codemod.rb +138 -50
- data/lib/hecks/construct.rb +21 -7
- data/lib/hecks/corpus.rb +438 -0
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
- data/lib/hecks/doc/reference.rb +200 -31
- data/lib/hecks/embryonaut_bluebook.rb +38 -15
- 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 +155 -35
- data/lib/hecks/facade/json_door.rb +106 -25
- data/lib/hecks/facade/surface/aggregate_door.rb +50 -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 -19
- data/lib/hecks/forms/html.rb +51 -7
- data/lib/hecks/forms/index_renderer.rb +14 -2
- data/lib/hecks/forms/page.rb +14 -0
- data/lib/hecks/forms/params.rb +120 -23
- data/lib/hecks/forms/port_argument.rb +14 -2
- data/lib/hecks/forms/query_form_renderer.rb +65 -2
- data/lib/hecks/forms/record_renderer.rb +60 -2
- data/lib/hecks/forms/record_table.rb +28 -1
- data/lib/hecks/forms/reference_options.rb +24 -0
- data/lib/hecks/forms/value_object_shape.rb +13 -3
- data/lib/hecks/forms.rb +24 -4
- data/lib/hecks/fqn.rb +59 -1
- data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
- data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
- data/lib/hecks/framework/oidc.json +15 -0
- data/lib/hecks/framework.rb +79 -25
- data/lib/hecks/freezer.rb +27 -11
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +241 -106
- data/lib/hecks/fuzzing/combination_miner.rb +178 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +241 -45
- data/lib/hecks/fuzzing/coverage_campaign.rb +161 -0
- data/lib/hecks/fuzzing/differential.rb +192 -0
- data/lib/hecks/fuzzing/domain_generator.rb +871 -0
- data/lib/hecks/fuzzing/era_boundary.rb +68 -19
- data/lib/hecks/fuzzing/form_census.rb +121 -20
- data/lib/hecks/fuzzing/generated_domain_check.rb +171 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +45 -6
- data/lib/hecks/fuzzing/isolated_boot.rb +137 -80
- data/lib/hecks/fuzzing/nondeterministic.rb +79 -0
- data/lib/hecks/fuzzing/persistence_parity.rb +111 -21
- data/lib/hecks/fuzzing/properties/corrections.rb +34 -9
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +300 -51
- data/lib/hecks/fuzzing/properties/guards.rb +86 -42
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +80 -32
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +40 -35
- data/lib/hecks/fuzzing/properties/outbox.rb +70 -32
- data/lib/hecks/fuzzing/properties/querying.rb +82 -28
- data/lib/hecks/fuzzing/properties.rb +84 -51
- data/lib/hecks/fuzzing/qa_settings.rb +164 -0
- data/lib/hecks/fuzzing/replay.rb +241 -133
- data/lib/hecks/fuzzing/rotation_priority.rb +48 -28
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +139 -0
- data/lib/hecks/fuzzing/self_consistency.rb +315 -137
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +46 -40
- 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 +59 -32
- data/lib/hecks/fuzzing/sequence_generator.rb +158 -23
- data/lib/hecks/fuzzing/shrinker.rb +309 -0
- data/lib/hecks/fuzzing/structural_skips.rb +37 -130
- data/lib/hecks/fuzzing/sweep_depth.rb +12 -4
- data/lib/hecks/fuzzing/target_capabilities.rb +107 -29
- data/lib/hecks/fuzzing/value_generator.rb +110 -22
- data/lib/hecks/fuzzing.rb +5 -0
- data/lib/hecks/grammar/evolve.rb +188 -12
- data/lib/hecks/grammar.rb +53 -7
- data/lib/hecks/ir.rb +51 -20
- 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/hecksagon/hecksagon.bluebook +11 -0
- data/lib/hecks/language/oidc.json +5 -0
- data/lib/hecks/literal.rb +41 -9
- data/lib/hecks/naming.rb +112 -31
- data/lib/hecks/ports/access_control.rb +53 -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 +50 -11
- data/lib/hecks/ports/clock.rb +42 -23
- data/lib/hecks/ports/extraction.rb +16 -0
- data/lib/hecks/ports/identity_assignment.rb +23 -2
- data/lib/hecks/ports/identity_generation.rb +17 -3
- data/lib/hecks/ports/identity_resolution.rb +17 -1
- data/lib/hecks/ports/key_vault.port +6 -0
- data/lib/hecks/ports/key_vault.rb +58 -0
- 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 +34 -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 +95 -6
- 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 +48 -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 +58 -19
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +96 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +9 -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 +41 -8
- data/lib/hecks/ports/query/ordering.rb +21 -6
- data/lib/hecks/ports/query.rb +35 -0
- data/lib/hecks/ports.rb +1 -0
- data/lib/hecks/projections/bootstrap_table.rb +147 -0
- data/lib/hecks/projections/diagrams.rb +315 -79
- data/lib/hecks/projections/glossary/html.rb +91 -3
- data/lib/hecks/projections/glossary/markdown.rb +50 -3
- data/lib/hecks/projections/glossary/mermaid.rb +49 -1
- data/lib/hecks/projections/glossary/sections.rb +1 -1
- data/lib/hecks/projections/glossary/sentences.rb +110 -5
- data/lib/hecks/projections/glossary.rb +173 -31
- data/lib/hecks/projections/ir.rb +1 -1
- data/lib/hecks/projections/model/deviations.rb +62 -17
- data/lib/hecks/projections/model.rb +75 -21
- data/lib/hecks/projections/oidc.rb +45 -8
- data/lib/hecks/projections/parser_table.rb +53 -12
- data/lib/hecks/projections/reference.rb +15 -3
- data/lib/hecks/projections/rust_vocabulary.rb +646 -0
- data/lib/hecks/projections/shape.rb +10 -2
- data/lib/hecks/projections/statements.rb +69 -22
- data/lib/hecks/projections/vocabulary.rb +26 -9
- data/lib/hecks/projections.rb +5 -3
- data/lib/hecks/projector/cli_projector.rb +245 -37
- data/lib/hecks/projector/docs_projector.rb +154 -28
- data/lib/hecks/projector/exporter.rb +104 -29
- data/lib/hecks/projector/ir_projector.rb +7 -1
- data/lib/hecks/projector/narrate_projector.rb +144 -23
- data/lib/hecks/projector/target.rb +42 -18
- data/lib/hecks/projector.rb +86 -18
- data/lib/hecks/query_ir.rb +94 -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 +61 -15
- 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 +9 -3
- data/lib/hecks/router/namespace_installer.rb +16 -3
- data/lib/hecks/router.rb +56 -1
- data/lib/hecks/runtime/aggregate_lock.rb +20 -11
- data/lib/hecks/runtime/boot_gates.rb +21 -3
- data/lib/hecks/runtime/caller.rb +40 -8
- data/lib/hecks/runtime/capability_graph.rb +13 -2
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +40 -41
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +45 -67
- data/lib/hecks/runtime/command_interpreter.rb +195 -113
- data/lib/hecks/runtime/command_rules/admissibility.rb +231 -80
- data/lib/hecks/runtime/command_rules/arithmetic.rb +154 -96
- 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 +145 -55
- data/lib/hecks/runtime/command_rules.rb +3 -0
- data/lib/hecks/runtime/dependency_planning.rb +56 -11
- data/lib/hecks/runtime/dispatcher.rb +263 -92
- data/lib/hecks/runtime/entity_element.rb +304 -67
- data/lib/hecks/runtime/entity_interpreter.rb +149 -88
- data/lib/hecks/runtime/errors.rb +37 -23
- data/lib/hecks/runtime/event.rb +15 -10
- data/lib/hecks/runtime/identity.rb +91 -23
- data/lib/hecks/runtime/instance.rb +103 -18
- data/lib/hecks/runtime/interpreting.rb +24 -16
- data/lib/hecks/runtime/invocation.rb +358 -0
- data/lib/hecks/runtime/loader.rb +105 -19
- data/lib/hecks/runtime/outbox.rb +164 -26
- data/lib/hecks/runtime/policy_interpreter.rb +73 -60
- data/lib/hecks/runtime/port_operation_interpreter.rb +42 -19
- data/lib/hecks/runtime/query_interpreter.rb +93 -74
- data/lib/hecks/runtime/reaction_invocation.rb +73 -28
- data/lib/hecks/runtime/read_model_interpreter.rb +60 -44
- data/lib/hecks/runtime/rebuild_sweep.rb +32 -4
- data/lib/hecks/runtime/reference_hop.rb +48 -6
- data/lib/hecks/runtime/refusal_wording.rb +142 -115
- data/lib/hecks/runtime/registry/saga_persistence.rb +32 -21
- data/lib/hecks/runtime/registry/verification.rb +153 -28
- data/lib/hecks/runtime/registry.rb +202 -32
- data/lib/hecks/runtime/remote_dispatcher.rb +125 -24
- data/lib/hecks/runtime/routing.rb +36 -154
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +25 -27
- data/lib/hecks/runtime/saga_interpreter.rb +90 -76
- data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
- data/lib/hecks/runtime/tenant_check.rb +33 -13
- data/lib/hecks/runtime/tenant_scope.rb +23 -5
- data/lib/hecks/runtime/value/admission.rb +75 -30
- data/lib/hecks/runtime/value/coercion.rb +555 -142
- data/lib/hecks/runtime/value/entity_list_coercion.rb +132 -60
- data/lib/hecks/runtime/value.rb +71 -21
- data/lib/hecks/runtime.rb +39 -7
- data/lib/hecks/storehouse.rb +368 -72
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
- data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
- data/lib/hecks/version.rb +3 -3
- data/lib/hecks/vocabulary.rb +205 -4
- data/lib/hecks.rb +91 -11
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +19 -11
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +24 -12
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +38 -14
- metadata +28 -2
|
@@ -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
|
|
|
@@ -11,12 +11,20 @@ module Hecks
|
|
|
11
11
|
# version, recording a Layer-3 approval, and minting an era's name
|
|
12
12
|
# once (never recomputed after).
|
|
13
13
|
module EraStore
|
|
14
|
+
# Lists every era the domain holds, oldest first, refusing any whose text was edited.
|
|
15
|
+
#
|
|
14
16
|
# Every held text is verified against its raw-byte digest on the
|
|
15
17
|
# way out — an edited storage fact refuses rather than silently
|
|
16
|
-
# reporting "no drift". This is
|
|
18
|
+
# reporting "no drift". This is not the era name (which hashes
|
|
17
19
|
# the canonical projection, minted once, never recomputed): it
|
|
18
|
-
# is a plain integrity check over bytes, and a row
|
|
19
|
-
#
|
|
20
|
+
# is a plain integrity check over bytes, and a row that carries
|
|
21
|
+
# no digest yet is backfilled, not refused.
|
|
22
|
+
#
|
|
23
|
+
# @return [Array<Hash{Symbol => Object}>] one Hash per era in ordinal order, `[]` when
|
|
24
|
+
# the domain holds none; keys are `:ordinal` (Integer), `:hash` and `:label` (String,
|
|
25
|
+
# nil until `mint_name!` names the era), `:held_text` (String) and `:watermark`
|
|
26
|
+
# (Integer journal ordinal the era was cut at, nil for era 1)
|
|
27
|
+
# @raise [Runtime::WiringError] if a held text no longer matches its stored digest
|
|
20
28
|
def eras
|
|
21
29
|
@db.exec_params(
|
|
22
30
|
"SELECT ordinal, hash, label, held_text, watermark, held_digest, held_projection::text " \
|
|
@@ -29,8 +37,15 @@ module Hecks
|
|
|
29
37
|
end
|
|
30
38
|
end
|
|
31
39
|
|
|
40
|
+
# Reads one era's stored row without running the integrity check that `eras` applies.
|
|
41
|
+
#
|
|
32
42
|
# The unverified row — what bin/reattest_era shows an operator
|
|
33
43
|
# after the integrity check fires.
|
|
44
|
+
#
|
|
45
|
+
# @param ordinal [Integer] ordinal of the era to read
|
|
46
|
+
# @return [Hash{Symbol => String, nil}, nil] `:held_text`, `:held_digest` (SHA-256 hex,
|
|
47
|
+
# nil if never stored), `:hash` (nil until minted) and `:held_projection` (JSON text,
|
|
48
|
+
# nil if never stored); nil when the domain holds no such era
|
|
34
49
|
def raw_era(ordinal)
|
|
35
50
|
rows = @db.exec_params(
|
|
36
51
|
"SELECT held_text, held_digest, hash, held_projection::text FROM hecks_eras WHERE domain = $1 AND ordinal = $2",
|
|
@@ -42,10 +57,17 @@ module Hecks
|
|
|
42
57
|
held_projection: rows[0]["held_projection"] }
|
|
43
58
|
end
|
|
44
59
|
|
|
45
|
-
#
|
|
60
|
+
# Accepts an era's held text as it now stands, replacing its stored digest.
|
|
61
|
+
#
|
|
62
|
+
# The recovery path: tamper-evidence (against accident and
|
|
46
63
|
# drift, not adversaries) gets resolved by a human acknowledging
|
|
47
64
|
# the text as it now stands — recorded in an append-only
|
|
48
|
-
# attestation table, never by a bare psql UPDATE
|
|
65
|
+
# attestation table, never by a bare psql `UPDATE`.
|
|
66
|
+
#
|
|
67
|
+
# @param ordinal [Integer] ordinal of the era to re-attest
|
|
68
|
+
# @return [String] the new SHA-256 hex digest of the held text
|
|
69
|
+
# @raise [Runtime::WiringError] if the domain holds no era with that ordinal
|
|
70
|
+
# @raise [PG::Error] if Postgres refuses the attestation DDL or a write
|
|
49
71
|
def reattest!(ordinal)
|
|
50
72
|
era = raw_era(ordinal)
|
|
51
73
|
raise Runtime::WiringError, "#{@domain} holds no era #{ordinal} to re-attest" unless era
|
|
@@ -72,6 +94,19 @@ module Hecks
|
|
|
72
94
|
fresh
|
|
73
95
|
end
|
|
74
96
|
|
|
97
|
+
# Checks one held text against its stored digest, storing the digest if the row has none.
|
|
98
|
+
#
|
|
99
|
+
# A text that passes (or that had no digest to fail against) also gets
|
|
100
|
+
# `backfill_frozen_facts!`; a text that fails gets nothing written.
|
|
101
|
+
#
|
|
102
|
+
# @param ordinal [Integer] ordinal of the era the text belongs to
|
|
103
|
+
# @param text [String] the era's held bluebook source text
|
|
104
|
+
# @param stored_digest [String, nil] SHA-256 hex digest stored with the row; nil when
|
|
105
|
+
# the row carries none yet
|
|
106
|
+
# @param stored_projection_json [String, nil] the row's stored projection as JSON text;
|
|
107
|
+
# nil when the row carries none yet
|
|
108
|
+
# @return [void]
|
|
109
|
+
# @raise [Runtime::WiringError] if `stored_digest` is present and does not match `text`
|
|
75
110
|
def verify_integrity!(ordinal, text, stored_digest, stored_projection_json)
|
|
76
111
|
digest = Digest::SHA256.hexdigest(text)
|
|
77
112
|
if stored_digest.nil?
|
|
@@ -90,9 +125,17 @@ module Hecks
|
|
|
90
125
|
raise Runtime::WiringError, Runtime::EraTamper.refusal(domain: @domain, ordinal: ordinal)
|
|
91
126
|
end
|
|
92
127
|
|
|
93
|
-
#
|
|
128
|
+
# Fills in the archive copy and the stored projection an era row is missing.
|
|
129
|
+
#
|
|
130
|
+
# A verified-authentic text backfills what a row may lack: its
|
|
94
131
|
# projection and its archive copy. Never run on a text that
|
|
95
132
|
# failed verification — that would bless the edit.
|
|
133
|
+
#
|
|
134
|
+
# @param ordinal [Integer] ordinal of the era the text belongs to
|
|
135
|
+
# @param text [String] the era's verified held source text
|
|
136
|
+
# @param stored_projection_json [String, nil] the row's stored projection as JSON text;
|
|
137
|
+
# nil makes this derive one from `text` and store it, unless `text` does not load
|
|
138
|
+
# @return [void]
|
|
96
139
|
def backfill_frozen_facts!(ordinal, text, stored_projection_json)
|
|
97
140
|
archive_text!(ordinal, text)
|
|
98
141
|
return if stored_projection_json
|
|
@@ -107,11 +150,27 @@ module Hecks
|
|
|
107
150
|
)
|
|
108
151
|
end
|
|
109
152
|
|
|
153
|
+
# Reports the ordinal of the newest era the domain holds.
|
|
154
|
+
#
|
|
155
|
+
# @return [Integer] the highest held ordinal, or 1 when no era is held yet
|
|
156
|
+
# @raise [Runtime::WiringError] if a held text no longer matches its stored digest
|
|
110
157
|
def current_era
|
|
111
158
|
held = eras
|
|
112
159
|
held.empty? ? 1 : held.last[:ordinal]
|
|
113
160
|
end
|
|
114
161
|
|
|
162
|
+
# Holds a domain's first source text as era 1 and makes era 1 the writable era.
|
|
163
|
+
#
|
|
164
|
+
# Call only while the domain holds no era — every caller checks `eras.empty?` first.
|
|
165
|
+
# An existing era 1 row is kept (`ON CONFLICT DO NOTHING`), but `advance_era!(1)` runs
|
|
166
|
+
# regardless, so calling this on a domain past era 1 would roll the write fence back.
|
|
167
|
+
#
|
|
168
|
+
# @param text [String] the bluebook source text to freeze as era 1
|
|
169
|
+
# @param projection [Hash{String => Object}, nil] the storage-shape projection of the
|
|
170
|
+
# bluebook, as `Runtime::StorageShape.project` returns it; nil stores none
|
|
171
|
+
# @return [void]
|
|
172
|
+
# @raise [PG::Error] if Postgres refuses the insert or the policy change, as it does
|
|
173
|
+
# for a role that does not own the journal
|
|
115
174
|
def hold_first!(text, projection: nil)
|
|
116
175
|
@db.exec_params(
|
|
117
176
|
"INSERT INTO hecks_eras (domain, ordinal, held_text, held_digest, held_projection) " \
|
|
@@ -124,6 +183,15 @@ module Hecks
|
|
|
124
183
|
advance_era!(1)
|
|
125
184
|
end
|
|
126
185
|
|
|
186
|
+
# Copies one version of an era's text into `hecks_era_texts`, keyed by its digest.
|
|
187
|
+
#
|
|
188
|
+
# A version already archived is left alone (`ON CONFLICT DO NOTHING`), so every
|
|
189
|
+
# distinct text an era ever held survives a re-attestation.
|
|
190
|
+
#
|
|
191
|
+
# @param ordinal [Integer] ordinal of the era the text belongs to
|
|
192
|
+
# @param text [String] the source text to archive
|
|
193
|
+
# @return [void]
|
|
194
|
+
# @raise [PG::Error] if Postgres refuses the insert
|
|
127
195
|
def archive_text!(ordinal, text)
|
|
128
196
|
@db.exec_params(
|
|
129
197
|
"INSERT INTO hecks_era_texts (domain, ordinal, digest, held_text) VALUES ($1, $2, $3, $4) " \
|
|
@@ -132,10 +200,19 @@ module Hecks
|
|
|
132
200
|
)
|
|
133
201
|
end
|
|
134
202
|
|
|
135
|
-
#
|
|
203
|
+
# Records that a human approved a translation edge's audit samples.
|
|
204
|
+
#
|
|
205
|
+
# A Layer-3 approval, recorded in the database it was reviewed
|
|
136
206
|
# against — bound to the edge's parsed content and the journal's
|
|
137
207
|
# high-water ordinal at review time. The latest row for a shape
|
|
138
208
|
# pair wins (re-approval supersedes).
|
|
209
|
+
#
|
|
210
|
+
# @param from [String] label of the era shape the edge leaves
|
|
211
|
+
# @param to [String] label of the era shape the edge leads to
|
|
212
|
+
# @param edge_digest [String] SHA-256 hex digest of the edge's parsed content, as
|
|
213
|
+
# `Translation::Audit.edge_digest` computes it
|
|
214
|
+
# @return [void]
|
|
215
|
+
# @raise [PG::Error] if Postgres refuses the insert
|
|
139
216
|
def record_approval!(from:, to:, edge_digest:)
|
|
140
217
|
@db.exec_params(
|
|
141
218
|
"INSERT INTO hecks_approvals (domain, from_label, to_label, edge_digest, reviewed_ordinal) " \
|
|
@@ -144,6 +221,13 @@ module Hecks
|
|
|
144
221
|
)
|
|
145
222
|
end
|
|
146
223
|
|
|
224
|
+
# Finds the most recent approval recorded for the edge between two era shapes.
|
|
225
|
+
#
|
|
226
|
+
# @param from [String] label of the era shape the edge leaves
|
|
227
|
+
# @param to [String] label of the era shape the edge leads to
|
|
228
|
+
# @return [Hash{Symbol => Object}, nil] `:edge_digest` (String, SHA-256 hex of the edge
|
|
229
|
+
# that was reviewed) and `:reviewed_ordinal` (Integer, the journal's highest ordinal at
|
|
230
|
+
# review time); nil when no approval is recorded for the pair
|
|
147
231
|
def approval_for(from:, to:)
|
|
148
232
|
rows = @db.exec_params(
|
|
149
233
|
"SELECT edge_digest, reviewed_ordinal FROM hecks_approvals " \
|
|
@@ -155,8 +239,16 @@ module Hecks
|
|
|
155
239
|
{ edge_digest: rows[0]["edge_digest"], reviewed_ordinal: rows[0]["reviewed_ordinal"].to_i }
|
|
156
240
|
end
|
|
157
241
|
|
|
158
|
-
#
|
|
242
|
+
# Stores an era's hash and label, only if the era has no hash yet.
|
|
243
|
+
#
|
|
244
|
+
# Era identity is minted once and stored; nothing ever recomputes
|
|
159
245
|
# a stored name to verify it.
|
|
246
|
+
#
|
|
247
|
+
# @param ordinal [Integer] ordinal of the era to name
|
|
248
|
+
# @param hash [String] the era's shape hash, SHA-256 hex
|
|
249
|
+
# @param label [String] the short label edges and refusals use, a prefix of `hash`
|
|
250
|
+
# @return [void]
|
|
251
|
+
# @raise [PG::Error] if Postgres refuses the update
|
|
160
252
|
def mint_name!(ordinal, hash, label)
|
|
161
253
|
@db.exec_params(
|
|
162
254
|
"UPDATE hecks_eras SET hash = $3, label = $4, canon_form = $5 " \
|
|
@@ -165,6 +257,9 @@ module Hecks
|
|
|
165
257
|
)
|
|
166
258
|
end
|
|
167
259
|
|
|
260
|
+
# Reads the journal's high-water mark across every era's partition.
|
|
261
|
+
#
|
|
262
|
+
# @return [Integer] the highest ordinal in the journal, or 0 when the journal is empty
|
|
168
263
|
def last_ordinal
|
|
169
264
|
@db.exec("SELECT COALESCE(max(ordinal), 0) AS o FROM #{quoted_journal}")[0]["o"].to_i
|
|
170
265
|
end
|