hecks 0.2.0 → 0.3.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 +5 -5
- data/lib/hecks/adapters/driven/claude_code.adapter +3 -0
- data/lib/hecks/adapters/driven/claude_code.rb +127 -0
- data/lib/hecks/adapters/driven/d1.adapter +12 -0
- data/lib/hecks/adapters/driven/d1.rb +403 -0
- data/lib/hecks/adapters/driven/folder.adapter +3 -0
- data/lib/hecks/adapters/driven/folder.rb +199 -0
- data/lib/hecks/adapters/driven/google_authentication.adapter +3 -0
- data/lib/hecks/adapters/driven/google_authentication.rb +100 -0
- data/lib/hecks/adapters/driven/governance_authorization.adapter +3 -0
- data/lib/hecks/adapters/driven/governance_authorization.rb +62 -0
- data/lib/hecks/adapters/driven/heki/journal.rb +51 -0
- data/lib/hecks/adapters/driven/heki/saga_store.rb +0 -0
- data/lib/hecks/adapters/driven/heki/snapshot.rb +38 -0
- data/lib/hecks/adapters/driven/heki.adapter +4 -0
- data/lib/hecks/adapters/driven/heki.rb +148 -0
- data/lib/hecks/adapters/driven/identity_registry.adapter +3 -0
- data/lib/hecks/adapters/driven/identity_registry.rb +26 -0
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +51 -0
- data/lib/hecks/adapters/driven/lambda/client.rb +63 -0
- data/lib/hecks/adapters/driven/lambda.adapter +4 -0
- data/lib/hecks/adapters/driven/lambda.rb +131 -0
- data/lib/hecks/adapters/driven/memory.adapter +3 -0
- data/lib/hecks/adapters/driven/memory.rb +103 -0
- data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +3 -0
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +28 -0
- data/lib/hecks/adapters/driven/postgres/codec.rb +80 -0
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +168 -0
- data/lib/hecks/adapters/driven/postgres.adapter +5 -0
- data/lib/hecks/adapters/driven/postgres.rb +391 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/era_store.rb +171 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/field_cache.rb +190 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/head_compiler.rb +429 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/mint_transaction.rb +166 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/provisioning.rb +286 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/resumable_backfill.rb +168 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/tail_merge.rb +163 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/transform_installer.rb +114 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage.rb +137 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/coverage_check.rb +89 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/era_resolver.rb +82 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/merge_coordinator.rb +43 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/minter.rb +132 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager.rb +74 -0
- data/lib/hecks/adapters/driven/postgres_era.adapter +6 -0
- data/lib/hecks/adapters/driven/postgres_era.rb +671 -0
- data/lib/hecks/adapters/driven/prism.adapter +3 -0
- data/lib/hecks/adapters/driven/prism.rb +80 -0
- data/lib/hecks/adapters/driven/secure_random_identity.adapter +3 -0
- data/lib/hecks/adapters/driven/secure_random_identity.rb +14 -0
- data/lib/hecks/adapters/driven/sql_query_builder.rb +221 -0
- data/lib/hecks/adapters/driven/sqlite/codec.rb +63 -0
- data/lib/hecks/adapters/driven/sqlite/projection.rb +105 -0
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +180 -0
- data/lib/hecks/adapters/driven/sqlite.adapter +9 -0
- data/lib/hecks/adapters/driven/sqlite.rb +285 -0
- data/lib/hecks/adapters/driven/system_clock.adapter +3 -0
- data/lib/hecks/adapters/driven/system_clock.rb +14 -0
- data/lib/hecks/adapters/driven.rb +36 -0
- data/lib/hecks/adapters.rb +6 -0
- data/lib/hecks/behaviors/dsl.rb +96 -0
- data/lib/hecks/behaviors/expectations.rb +280 -0
- data/lib/hecks/behaviors/ir.rb +31 -0
- data/lib/hecks/behaviors/rspec.rb +42 -0
- data/lib/hecks/behaviors/runner.rb +96 -0
- data/lib/hecks/behaviors.rb +25 -0
- data/lib/hecks/bluebook/aggregate.rb +108 -0
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +134 -0
- data/lib/hecks/bluebook/assembly/build.rb +48 -0
- data/lib/hecks/bluebook/assembly/contract.rb +112 -0
- data/lib/hecks/bluebook/assembly/contracts.rb +409 -0
- data/lib/hecks/bluebook/assembly/marks.rb +227 -0
- data/lib/hecks/bluebook/assembly/specializer.rb +70 -0
- data/lib/hecks/bluebook/assembly.rb +78 -0
- data/lib/hecks/bluebook/attribute.rb +96 -0
- data/lib/hecks/bluebook/behaviour/aggregate.rb +83 -0
- data/lib/hecks/bluebook/behaviour/attribute.rb +27 -0
- data/lib/hecks/bluebook/behaviour/chapter.rb +72 -0
- data/lib/hecks/bluebook/behaviour/command.rb +116 -0
- data/lib/hecks/bluebook/behaviour/domain_port.rb +25 -0
- data/lib/hecks/bluebook/behaviour/entity.rb +59 -0
- data/lib/hecks/bluebook/behaviour/hexagon.rb +50 -0
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +51 -0
- data/lib/hecks/bluebook/behaviour/policy.rb +52 -0
- data/lib/hecks/bluebook/behaviour/process_manager.rb +38 -0
- data/lib/hecks/bluebook/behaviour/query.rb +10 -0
- data/lib/hecks/bluebook/behaviour/read_model.rb +29 -0
- data/lib/hecks/bluebook/behaviour/traits.rb +81 -0
- data/lib/hecks/bluebook/behaviour/value_object.rb +33 -0
- data/lib/hecks/bluebook/chapter.rb +68 -0
- data/lib/hecks/bluebook/command.rb +124 -0
- data/lib/hecks/bluebook/domain_port.rb +102 -0
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +34 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +906 -0
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +327 -0
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +71 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +1018 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +626 -0
- data/lib/hecks/bluebook/dsl/const_shim.rb +81 -0
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +79 -0
- data/lib/hecks/bluebook/dsl/entity_builder.rb +305 -0
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +366 -0
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +163 -0
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +191 -0
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +44 -0
- data/lib/hecks/bluebook/dsl/malformed.rb +7 -0
- data/lib/hecks/bluebook/dsl/policy_builder.rb +120 -0
- data/lib/hecks/bluebook/dsl/port_builder.rb +30 -0
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +142 -0
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +207 -0
- data/lib/hecks/bluebook/dsl/query_builder.rb +113 -0
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +233 -0
- data/lib/hecks/bluebook/dsl/rule_reference.rb +172 -0
- data/lib/hecks/bluebook/dsl/translation_builder.rb +243 -0
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +178 -0
- data/lib/hecks/bluebook/dsl/word_gate.rb +221 -0
- data/lib/hecks/bluebook/dsl/world_builder.rb +77 -0
- data/lib/hecks/bluebook/dsl.rb +45 -0
- data/lib/hecks/bluebook/entity.rb +103 -0
- data/lib/hecks/bluebook/expression/canonical_form.rb +55 -0
- data/lib/hecks/bluebook/expression/evaluator.rb +261 -0
- data/lib/hecks/bluebook/expression/projection.json +170 -0
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +233 -0
- data/lib/hecks/bluebook/expression/resolver.rb +630 -0
- data/lib/hecks/bluebook/expression.rb +13 -0
- data/lib/hecks/bluebook/hexagon.rb +60 -0
- data/lib/hecks/bluebook/lifecycle.rb +42 -0
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +54 -0
- data/lib/hecks/bluebook/meta_validator/judge.rb +559 -0
- data/lib/hecks/bluebook/meta_validator/plan.rb +332 -0
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +47 -0
- data/lib/hecks/bluebook/meta_validator/readings.rb +350 -0
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +312 -0
- data/lib/hecks/bluebook/meta_validator/shapes.rb +246 -0
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +255 -0
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +138 -0
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +78 -0
- data/lib/hecks/bluebook/meta_validator.rb +452 -0
- data/lib/hecks/bluebook/model_check.rb +318 -0
- data/lib/hecks/bluebook/pattern_subset.rb +120 -0
- data/lib/hecks/bluebook/policy.rb +41 -0
- data/lib/hecks/bluebook/process_manager.rb +86 -0
- data/lib/hecks/bluebook/project_discovery.rb +30 -0
- data/lib/hecks/bluebook/project_loader.rb +40 -0
- data/lib/hecks/bluebook/project_register.rb +82 -0
- data/lib/hecks/bluebook/query.rb +61 -0
- data/lib/hecks/bluebook/read_model.rb +109 -0
- data/lib/hecks/bluebook/reference.rb +74 -0
- data/lib/hecks/bluebook/smoke_test.rb +166 -0
- data/lib/hecks/bluebook/synthesizer.rb +95 -0
- data/lib/hecks/bluebook/translation.rb +92 -0
- data/lib/hecks/bluebook/value_object.rb +58 -0
- data/lib/hecks/bluebook.rb +74 -0
- data/lib/hecks/codemod.rb +342 -0
- data/lib/hecks/construct.rb +71 -0
- data/lib/hecks/deploy/bluebook/deploy.bluebook +219 -0
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +4 -0
- data/lib/hecks/deploy/oidc.json +18 -0
- data/lib/hecks/doc/reference.rb +389 -0
- data/lib/hecks/embryonaut_bluebook.rb +75 -0
- data/lib/hecks/facade/cli_door.rb +116 -0
- data/lib/hecks/facade/cli_runner.rb +190 -0
- data/lib/hecks/facade/command_request.rb +105 -0
- data/lib/hecks/facade/handle.rb +173 -0
- data/lib/hecks/facade/json_door.rb +154 -0
- data/lib/hecks/facade/surface/aggregate_door.rb +185 -0
- data/lib/hecks/facade/surface/chapter.rb +107 -0
- data/lib/hecks/facade/surface.rb +48 -0
- data/lib/hecks/facade.rb +44 -0
- data/lib/hecks/forms/app.rb +300 -0
- data/lib/hecks/forms/command_form_renderer.rb +113 -0
- data/lib/hecks/forms/examples/banking_console.bluebook +3 -0
- data/lib/hecks/forms/field_renderer.rb +170 -0
- data/lib/hecks/forms/field_shape.rb +232 -0
- data/lib/hecks/forms/html.rb +53 -0
- data/lib/hecks/forms/index_renderer.rb +35 -0
- data/lib/hecks/forms/page.rb +157 -0
- data/lib/hecks/forms/params.rb +132 -0
- data/lib/hecks/forms/query_form_renderer.rb +114 -0
- data/lib/hecks/forms/record_renderer.rb +115 -0
- data/lib/hecks/forms/record_table.rb +63 -0
- data/lib/hecks/forms/reference_options.rb +30 -0
- data/lib/hecks/forms/value_object_shape.rb +46 -0
- data/lib/hecks/forms.rb +54 -0
- data/lib/hecks/fqn.rb +94 -0
- data/lib/hecks/framework/bluebook/compliance.bluebook +1 -0
- data/lib/hecks/framework/bluebook/console_settings.bluebook +489 -0
- data/lib/hecks/framework/bluebook/framework.hecksagon +32 -0
- data/lib/hecks/framework/bluebook/governance.bluebook +130 -0
- data/lib/hecks/framework/bluebook/identity.bluebook +90 -0
- data/lib/hecks/framework/oidc.json +39 -0
- data/lib/hecks/framework.rb +90 -0
- data/lib/hecks/freezer.rb +67 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +86 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +92 -0
- data/lib/hecks/fuzzing/properties.rb +1146 -0
- data/lib/hecks/fuzzing/replay.rb +635 -0
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +93 -0
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +54 -0
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +96 -0
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +185 -0
- data/lib/hecks/fuzzing/sequence_generator.rb +130 -0
- data/lib/hecks/fuzzing/value_generator.rb +177 -0
- data/lib/hecks/fuzzing.rb +12 -0
- data/lib/hecks/grammar/evolve.rb +291 -0
- data/lib/hecks/grammar/expression.bluebook +417 -0
- data/lib/hecks/grammar/expression_operators.json +1152 -0
- data/lib/hecks/grammar/grammar.hecksagon +20 -0
- data/lib/hecks/grammar/oidc.json +69 -0
- data/lib/hecks/grammar/translation.bluebook +203 -0
- data/lib/hecks/grammar.rb +178 -0
- data/lib/hecks/ir.rb +126 -0
- data/lib/hecks/language/adapter.bluebook +116 -0
- data/lib/hecks/language/bluebook/aggregate.bluebook +581 -0
- data/lib/hecks/language/bluebook/attaches/paging.bluebook +76 -0
- data/lib/hecks/language/bluebook/bluebook.bluebook +256 -0
- data/lib/hecks/language/bluebook/bluebook.hecksagon +24 -0
- data/lib/hecks/language/bluebook/command.bluebook +437 -0
- data/lib/hecks/language/bluebook/entity.bluebook +316 -0
- data/lib/hecks/language/bluebook/policy.bluebook +172 -0
- data/lib/hecks/language/bluebook/process_manager.bluebook +264 -0
- data/lib/hecks/language/bluebook/projection.bluebook +267 -0
- data/lib/hecks/language/bluebook/query.bluebook +245 -0
- data/lib/hecks/language/bluebook/shape.bluebook +292 -0
- data/lib/hecks/language/bluebook/syntax.bluebook +447 -0
- data/lib/hecks/language/bluebook/vocabulary.bluebook +364 -0
- data/lib/hecks/language/hecksagon/adapter_binding.bluebook +51 -0
- data/lib/hecks/language/hecksagon/domain_port.bluebook +76 -0
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +131 -0
- data/lib/hecks/language/hecksagon/port_operation.bluebook +102 -0
- data/lib/hecks/language/oidc.json +313 -0
- data/lib/hecks/language/port.bluebook +92 -0
- data/lib/hecks/language/translation/translation.bluebook +110 -0
- data/lib/hecks/language/translation/translation_aggregate.bluebook +267 -0
- data/lib/hecks/language/world/wiring.bluebook +62 -0
- data/lib/hecks/language/world/world.bluebook +84 -0
- data/lib/hecks/literal.rb +125 -0
- data/lib/hecks/naming.rb +121 -0
- data/lib/hecks/ports/access_control.port +4 -0
- data/lib/hecks/ports/access_control.rb +62 -0
- data/lib/hecks/ports/agent/answers.rb +104 -0
- data/lib/hecks/ports/agent.port +4 -0
- data/lib/hecks/ports/agent.rb +167 -0
- data/lib/hecks/ports/authentication.port +4 -0
- data/lib/hecks/ports/authentication.rb +50 -0
- data/lib/hecks/ports/authorization.port +4 -0
- data/lib/hecks/ports/authorization.rb +59 -0
- data/lib/hecks/ports/clock.port +4 -0
- data/lib/hecks/ports/clock.rb +62 -0
- data/lib/hecks/ports/extraction.port +4 -0
- data/lib/hecks/ports/extraction.rb +37 -0
- data/lib/hecks/ports/identity_assignment.port +4 -0
- data/lib/hecks/ports/identity_assignment.rb +45 -0
- data/lib/hecks/ports/identity_generation.port +4 -0
- data/lib/hecks/ports/identity_generation.rb +49 -0
- data/lib/hecks/ports/identity_resolution.port +4 -0
- data/lib/hecks/ports/identity_resolution.rb +40 -0
- data/lib/hecks/ports/loading.port +4 -0
- data/lib/hecks/ports/loading.rb +13 -0
- data/lib/hecks/ports/persistence/append_only.rb +102 -0
- data/lib/hecks/ports/persistence/binding_policy.rb +56 -0
- data/lib/hecks/ports/persistence/execution.rb +18 -0
- data/lib/hecks/ports/persistence/lineage.rb +277 -0
- data/lib/hecks/ports/persistence/null_saga_store.rb +25 -0
- data/lib/hecks/ports/persistence/remote_runtime.rb +42 -0
- data/lib/hecks/ports/persistence/repository_factory.rb +30 -0
- data/lib/hecks/ports/persistence.port +4 -0
- data/lib/hecks/ports/persistence.rb +39 -0
- data/lib/hecks/ports/projection.port +4 -0
- data/lib/hecks/ports/projection.rb +76 -0
- data/lib/hecks/ports/query/in_memory.rb +60 -0
- data/lib/hecks/ports/query/ordering.rb +41 -0
- data/lib/hecks/ports/query.rb +36 -0
- data/lib/hecks/ports.rb +27 -0
- data/lib/hecks/projections/diagrams.rb +413 -0
- data/lib/hecks/projections/ir.rb +18 -0
- data/lib/hecks/projections/model/deviations.rb +98 -0
- data/lib/hecks/projections/model.rb +145 -0
- data/lib/hecks/projections/oidc.rb +110 -0
- data/lib/hecks/projections/parser_table.rb +159 -0
- data/lib/hecks/projections/reference.rb +38 -0
- data/lib/hecks/projections/shape.rb +31 -0
- data/lib/hecks/projections/statements.rb +110 -0
- data/lib/hecks/projections/vocabulary.rb +100 -0
- data/lib/hecks/projections.rb +32 -0
- data/lib/hecks/projector/cli_projector.rb +437 -0
- data/lib/hecks/projector/docs_projector.rb +321 -0
- data/lib/hecks/projector/exporter.rb +141 -0
- data/lib/hecks/projector/ir_projector.rb +18 -0
- data/lib/hecks/projector/narrate_projector.rb +243 -0
- data/lib/hecks/projector/target.rb +97 -0
- data/lib/hecks/projector.rb +186 -0
- data/lib/hecks/query_ir.rb +392 -0
- data/lib/hecks/query_specification/common/authorization_spec.rb +9 -0
- data/lib/hecks/query_specification/common/comparators.rb +25 -0
- data/lib/hecks/query_specification/common/comparison.rb +174 -0
- data/lib/hecks/query_specification/common/cursor_spec.rb +9 -0
- data/lib/hecks/query_specification/common/dsl.rb +58 -0
- data/lib/hecks/query_specification/common/inspection_spec.rb +9 -0
- data/lib/hecks/query_specification/common/limit_spec.rb +9 -0
- data/lib/hecks/query_specification/common/null_policy.rb +80 -0
- data/lib/hecks/query_specification/common/null_semantics.rb +11 -0
- data/lib/hecks/query_specification/common/offset_spec.rb +9 -0
- data/lib/hecks/query_specification/common/options.rb +32 -0
- data/lib/hecks/query_specification/common/order_by.rb +9 -0
- data/lib/hecks/query_specification/common/specification.rb +10 -0
- data/lib/hecks/query_specification/common/where_clause.rb +9 -0
- data/lib/hecks/query_specification/field_path.rb +89 -0
- data/lib/hecks/query_specification/hop_path.rb +130 -0
- data/lib/hecks/query_specification/read_model/specification.rb +18 -0
- data/lib/hecks/query_specification.rb +14 -0
- data/lib/hecks/rendering.rb +48 -0
- data/lib/hecks/router/namespace_installer.rb +157 -0
- data/lib/hecks/router.rb +70 -0
- data/lib/hecks/runtime/caller.rb +50 -0
- data/lib/hecks/runtime/capability_graph.rb +44 -0
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +124 -0
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +248 -0
- data/lib/hecks/runtime/command_interpreter.rb +413 -0
- data/lib/hecks/runtime/command_rules/admissibility.rb +307 -0
- data/lib/hecks/runtime/command_rules/arithmetic.rb +254 -0
- data/lib/hecks/runtime/command_rules/authorization.rb +64 -0
- data/lib/hecks/runtime/command_rules/emission.rb +34 -0
- data/lib/hecks/runtime/command_rules/references.rb +181 -0
- data/lib/hecks/runtime/command_rules.rb +28 -0
- data/lib/hecks/runtime/dependency_planning.rb +231 -0
- data/lib/hecks/runtime/dispatcher.rb +286 -0
- data/lib/hecks/runtime/entity_element.rb +253 -0
- data/lib/hecks/runtime/entity_interpreter.rb +228 -0
- data/lib/hecks/runtime/era_check.rb +136 -0
- data/lib/hecks/runtime/era_guard/shape_diff.rb +128 -0
- data/lib/hecks/runtime/era_guard.rb +193 -0
- data/lib/hecks/runtime/era_tamper.rb +61 -0
- data/lib/hecks/runtime/errors.rb +100 -0
- data/lib/hecks/runtime/event.rb +51 -0
- data/lib/hecks/runtime/identity.rb +128 -0
- data/lib/hecks/runtime/instance.rb +120 -0
- data/lib/hecks/runtime/interpreting.rb +69 -0
- data/lib/hecks/runtime/loader.rb +143 -0
- data/lib/hecks/runtime/policy_interpreter.rb +366 -0
- data/lib/hecks/runtime/port_operation_interpreter.rb +210 -0
- data/lib/hecks/runtime/query_interpreter.rb +254 -0
- data/lib/hecks/runtime/reaction_invocation.rb +245 -0
- data/lib/hecks/runtime/read_model_interpreter.rb +266 -0
- data/lib/hecks/runtime/rebuild_sweep.rb +74 -0
- data/lib/hecks/runtime/reference_hop.rb +99 -0
- data/lib/hecks/runtime/refusal_wording.rb +124 -0
- data/lib/hecks/runtime/registry/saga_persistence.rb +70 -0
- data/lib/hecks/runtime/registry/verification.rb +158 -0
- data/lib/hecks/runtime/registry.rb +218 -0
- data/lib/hecks/runtime/remote_dispatcher.rb +143 -0
- data/lib/hecks/runtime/routing.rb +96 -0
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +97 -0
- data/lib/hecks/runtime/saga_interpreter.rb +301 -0
- data/lib/hecks/runtime/storage_shape.rb +120 -0
- data/lib/hecks/runtime/tenant_check.rb +84 -0
- data/lib/hecks/runtime/tenant_scope.rb +55 -0
- data/lib/hecks/runtime/value/admission.rb +112 -0
- data/lib/hecks/runtime/value/coercion.rb +500 -0
- data/lib/hecks/runtime/value/invariant_violation.rb +5 -0
- data/lib/hecks/runtime/value.rb +125 -0
- data/lib/hecks/runtime.rb +95 -0
- data/lib/hecks/translation/audit/approval_digest.rb +31 -0
- data/lib/hecks/translation/audit/layer_one.rb +37 -0
- data/lib/hecks/translation/audit/layer_two.rb +77 -0
- data/lib/hecks/translation/audit/unfed_report.rb +42 -0
- data/lib/hecks/translation/audit.rb +70 -0
- data/lib/hecks/translation/reattest.rb +72 -0
- data/lib/hecks/translation/rule_compiler.rb +120 -0
- data/lib/hecks/translation/scaffold/differ.rb +183 -0
- data/lib/hecks/translation/scaffold/renderer.rb +41 -0
- data/lib/hecks/translation/scaffold/writer.rb +28 -0
- data/lib/hecks/translation/scaffold.rb +30 -0
- data/lib/hecks/translation.rb +11 -0
- data/lib/hecks/version.rb +16 -0
- data/lib/hecks/vocabulary.rb +213 -0
- data/lib/hecks.rb +118 -6
- metadata +401 -222
- data/bin/hecks +0 -7
- data/bin/hecks-package +0 -65
- data/bin/hecks_console +0 -12
- data/bin/hecks_serverless +0 -6
- data/lib/cli/build.rb +0 -14
- data/lib/cli/command_runner.rb +0 -28
- data/lib/cli/console.rb +0 -10
- data/lib/cli/generate.rb +0 -37
- data/lib/cli/hecks-cli.rb +0 -27
- data/lib/cli/test.rb +0 -57
- data/lib/console/commands.rb +0 -8
- data/lib/console/hecks-console.rb +0 -1
- data/lib/packager/README.md +0 -0
- data/lib/packager/app_runner.rb +0 -21
- data/lib/packager/args.rb +0 -26
- data/lib/packager/compatibility/fixnum.rb +0 -6
- data/lib/packager/hecks.rb +0 -39
- data/lib/packager/query_runner.rb +0 -21
- data/lib/packager/resources/Dockerfile +0 -11
- data/lib/packager/resources/app_binary +0 -7
- data/lib/packager/resources/bundle_config +0 -3
- data/lib/packager/resources/traveling-ruby-20150715-2.2.2-linux-x86_64.tar.gz +0 -0
- data/lib/packager/resources/traveling-ruby-20150715-2.2.2-osx.tar.gz +0 -0
- data/lib/serverless/Domain +0 -32
- data/lib/serverless/cli.rb +0 -75
- data/lib/serverless/resources/command_name.js +0 -5
- data/lib/serverless/resources/environment.js +0 -7
- data/lib/serverless/resources/handler.js.tt +0 -28
- data/lib/serverless/resources/run_binary.js +0 -22
- data/lib/serverless/resources/serverless.yml +0 -20
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
require_relative "../../../../runtime/registry"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Adapters
|
|
5
|
+
class PostgresEra
|
|
6
|
+
class Lineage
|
|
7
|
+
module TailMerge
|
|
8
|
+
# ── fork observability ─────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
# Post-cut writes an old era made after the newer era was minted
|
|
11
|
+
# — the divergence between the worlds, observable at any time.
|
|
12
|
+
def diverged_count(old_era)
|
|
13
|
+
cut = eras.find { |era| era[:ordinal] == old_era + 1 }&.dig(:watermark)
|
|
14
|
+
return 0 unless cut
|
|
15
|
+
|
|
16
|
+
@db.exec(
|
|
17
|
+
"SELECT count(*) FROM #{quoted_journal} WHERE era = #{old_era.to_i} AND ordinal > #{cut.to_i}"
|
|
18
|
+
)[0]["count"].to_i
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# ── tail-merge ─────────────────────────────────────────────────
|
|
22
|
+
#
|
|
23
|
+
# The one deliberate command — it marks a business event (an app
|
|
24
|
+
# retiring), never a shape change. One transaction: advance the
|
|
25
|
+
# watermarks, rebuild the head so the tail interleaves by its
|
|
26
|
+
# recorded global ordinal, append the declared winners, audit —
|
|
27
|
+
# and roll the whole thing back on any refusal. Records touched
|
|
28
|
+
# by both worlds since the cut refuse until each has an explicit
|
|
29
|
+
# winner; resolution itself is append-only (the winner's state
|
|
30
|
+
# re-enters as the newest row and wins structurally — originals
|
|
31
|
+
# stay immutable).
|
|
32
|
+
def merge_tail!(aggregates:, edges:, winners: {}, audit: nil)
|
|
33
|
+
@db.exec("BEGIN")
|
|
34
|
+
@db.exec("SET LOCAL lock_timeout = '10s'")
|
|
35
|
+
@db.exec("SELECT pg_advisory_xact_lock(hashtext('hecks_eras:' || #{text_literal(@domain)}))")
|
|
36
|
+
held = eras
|
|
37
|
+
era = held.last[:ordinal]
|
|
38
|
+
label = held.last[:label]
|
|
39
|
+
if era == 1
|
|
40
|
+
@db.exec("ROLLBACK")
|
|
41
|
+
raise Runtime::WiringError, "nothing to merge — #{@domain} stands at era 1"
|
|
42
|
+
end
|
|
43
|
+
cut = held.last[:watermark].to_i
|
|
44
|
+
tip = last_ordinal
|
|
45
|
+
|
|
46
|
+
conflicts = aggregates.flat_map { |aggregate| conflict_ids(aggregate, edges, era, cut) }
|
|
47
|
+
unresolved = conflicts.reject { |_, id| winners.key?(id) }
|
|
48
|
+
unless unresolved.empty?
|
|
49
|
+
@db.exec("ROLLBACK")
|
|
50
|
+
raise Runtime::WiringError,
|
|
51
|
+
"cannot merge the tail of #{@domain}: touched by both worlds since the cut — " \
|
|
52
|
+
"#{unresolved.map { |storage, id| "#{storage}##{id}" }.sort.join(', ')}. " \
|
|
53
|
+
"Name each winner (--winner <id>=old or --winner <id>=new), then run bin/merge_tail again. " \
|
|
54
|
+
"A winner takes the WHOLE record — the aggregate is the consistency boundary, so the " \
|
|
55
|
+
"loser's edits are discarded even where they touched different attributes"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# the new world's pre-merge head states, captured before the
|
|
59
|
+
# rebuild lets the tail interleave
|
|
60
|
+
new_states = {}
|
|
61
|
+
aggregates.each do |aggregate|
|
|
62
|
+
winners.select { |_, side| side == "new" }.each_key do |id|
|
|
63
|
+
row = @db.exec_params("SELECT state FROM #{quote(head_view(aggregate.storage_name))} WHERE id = $1", [id])
|
|
64
|
+
new_states[id] = [aggregate.storage_name, row[0]["state"]] if row.ntuples.positive?
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
@db.exec_params("UPDATE hecks_eras SET watermark = $2 WHERE domain = $1 AND ordinal > 1", [@domain, tip])
|
|
69
|
+
aggregates.each do |aggregate|
|
|
70
|
+
@db.exec("DROP VIEW IF EXISTS #{quote(head_view(aggregate.storage_name))}")
|
|
71
|
+
@db.exec("DROP MATERIALIZED VIEW IF EXISTS #{quote(matview(aggregate.storage_name, era, label))}")
|
|
72
|
+
# full: the watermarks just moved — every ancestor matview's
|
|
73
|
+
# cut is stale, so there is nothing safe to layer on.
|
|
74
|
+
compile_head!(aggregate, era, label, edges, full: true)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
winners.each do |id, side|
|
|
78
|
+
aggregates.each do |aggregate|
|
|
79
|
+
state =
|
|
80
|
+
if side == "old"
|
|
81
|
+
row = @db.exec_params(
|
|
82
|
+
"SELECT state FROM #{quote(matview(aggregate.storage_name, era, label))} " \
|
|
83
|
+
"WHERE aggregate_id = $1 AND operation = 'save' ORDER BY ordinal DESC LIMIT 1",
|
|
84
|
+
[id]
|
|
85
|
+
)
|
|
86
|
+
row.ntuples.positive? ? row[0]["state"] : nil
|
|
87
|
+
else
|
|
88
|
+
new_states[id]&.first == aggregate.storage_name ? new_states[id][1] : nil
|
|
89
|
+
end
|
|
90
|
+
next unless state
|
|
91
|
+
|
|
92
|
+
# Same two-step append does for a live write — journal
|
|
93
|
+
# first, snapshot second — because this INSERT bypasses
|
|
94
|
+
# PostgresEra#append entirely (it writes through Lineage
|
|
95
|
+
# directly). Skipping the snapshot half here would mean a
|
|
96
|
+
# merge winner lands in the journal but head_view — which
|
|
97
|
+
# reads this era's live rows from the snapshot table, not
|
|
98
|
+
# by re-scanning the journal — never shows it.
|
|
99
|
+
ordinal = @db.exec_params(
|
|
100
|
+
"INSERT INTO #{quoted_journal} (era, aggregate, aggregate_id, operation, state) " \
|
|
101
|
+
"VALUES ($1, $2, $3, 'save', $4) RETURNING ordinal",
|
|
102
|
+
[era, aggregate.storage_name, id, state]
|
|
103
|
+
)[0]["ordinal"]
|
|
104
|
+
@db.exec_params(
|
|
105
|
+
"INSERT INTO #{quote(head_snapshot(aggregate.storage_name, era))} (id, ordinal, state) VALUES ($1, $2, $3) " \
|
|
106
|
+
"ON CONFLICT (id) DO UPDATE SET ordinal = EXCLUDED.ordinal, state = EXCLUDED.state " \
|
|
107
|
+
"WHERE #{quote(head_snapshot(aggregate.storage_name, era))}.ordinal < EXCLUDED.ordinal",
|
|
108
|
+
[id, ordinal, state]
|
|
109
|
+
)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
if audit
|
|
114
|
+
violations = audit.call
|
|
115
|
+
unless violations.empty?
|
|
116
|
+
@db.exec("ROLLBACK")
|
|
117
|
+
raise Runtime::WiringError,
|
|
118
|
+
"cannot merge the tail of #{@domain}: the audit refused —\n - #{violations.join("\n - ")}"
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
@db.exec("COMMIT")
|
|
123
|
+
true
|
|
124
|
+
rescue PG::LockNotAvailable
|
|
125
|
+
@db.exec("ROLLBACK") rescue nil
|
|
126
|
+
raise Runtime::WiringError,
|
|
127
|
+
"cannot merge the tail of #{@domain}: another mint or merge holds the domain lock — " \
|
|
128
|
+
"waited 10s; try again shortly"
|
|
129
|
+
rescue PG::Error => error
|
|
130
|
+
@db.exec("ROLLBACK") rescue nil
|
|
131
|
+
raise Runtime::WiringError, "cannot merge the tail of #{@domain}: #{error.message.strip}"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Ids touched by BOTH worlds since the cut — the old world's
|
|
135
|
+
# post-cut tail INTERSECTed with the new world's own writes.
|
|
136
|
+
#
|
|
137
|
+
# KNOWN GAP, not silently risked: this compares raw
|
|
138
|
+
# `aggregate_id` values, with no notion of "these two different
|
|
139
|
+
# ids are the same entity, rekeyed." If a domain's history
|
|
140
|
+
# includes a rekey (see TranslationRekey) and is LATER
|
|
141
|
+
# merged here, a record's pre-rekey and post-rekey rows will
|
|
142
|
+
# never intersect — they just silently survive as two separate,
|
|
143
|
+
# unrelated-looking heads (a duplicate, not corruption: nothing
|
|
144
|
+
# here deletes or clobbers either side). Resolve any such
|
|
145
|
+
# duplicate manually after a merge; teaching this INTERSECT
|
|
146
|
+
# about a rekey mapping is real, separate work, deliberately
|
|
147
|
+
# out of scope for rekey's first pass.
|
|
148
|
+
def conflict_ids(aggregate, edges, era, cut)
|
|
149
|
+
names = names_by_era(aggregate, edges)
|
|
150
|
+
olds = (1...era).map { |ancestor| text_literal(names[:storage][ancestor - 1]) }.join(", ")
|
|
151
|
+
@db.exec(<<~SQL).map { |row| [aggregate.storage_name, row["aggregate_id"]] }
|
|
152
|
+
SELECT aggregate_id FROM #{quoted_journal}
|
|
153
|
+
WHERE era < #{era.to_i} AND aggregate IN (#{olds}) AND ordinal > #{cut.to_i}
|
|
154
|
+
INTERSECT
|
|
155
|
+
SELECT aggregate_id FROM #{quoted_journal}
|
|
156
|
+
WHERE era = #{era.to_i} AND aggregate = #{text_literal(names[:storage][era - 1])}
|
|
157
|
+
SQL
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Adapters
|
|
3
|
+
class PostgresEra
|
|
4
|
+
class Lineage
|
|
5
|
+
module TransformInstaller
|
|
6
|
+
# The jsonb rule transforms — installed once, idempotently. Kept
|
|
7
|
+
# equal to the port's reference entry-JSON transform by the
|
|
8
|
+
# cross-execution equivalence spec; the SQL here is a compilation
|
|
9
|
+
# target, not a second source of truth.
|
|
10
|
+
def install_transforms!
|
|
11
|
+
@db.exec(<<~SQL)
|
|
12
|
+
CREATE OR REPLACE FUNCTION hecks_tr_extract(state jsonb, path text[], OUT remaining jsonb, OUT value jsonb, OUT present boolean)
|
|
13
|
+
LANGUAGE plpgsql IMMUTABLE AS $fn$
|
|
14
|
+
DECLARE parent jsonb; leaf text;
|
|
15
|
+
BEGIN
|
|
16
|
+
remaining := state;
|
|
17
|
+
present := false;
|
|
18
|
+
leaf := path[array_upper(path, 1)];
|
|
19
|
+
IF array_length(path, 1) = 1 THEN
|
|
20
|
+
IF state ? leaf THEN
|
|
21
|
+
present := true;
|
|
22
|
+
value := state -> leaf;
|
|
23
|
+
remaining := state - leaf;
|
|
24
|
+
END IF;
|
|
25
|
+
RETURN;
|
|
26
|
+
END IF;
|
|
27
|
+
parent := state #> path[1:array_upper(path, 1) - 1];
|
|
28
|
+
IF jsonb_typeof(parent) = 'object' AND parent ? leaf THEN
|
|
29
|
+
present := true;
|
|
30
|
+
value := parent -> leaf;
|
|
31
|
+
parent := parent - leaf;
|
|
32
|
+
IF parent = '{}'::jsonb THEN
|
|
33
|
+
remaining := state - path[1];
|
|
34
|
+
ELSE
|
|
35
|
+
remaining := jsonb_set(state, path[1:array_upper(path, 1) - 1], parent);
|
|
36
|
+
END IF;
|
|
37
|
+
END IF;
|
|
38
|
+
END $fn$
|
|
39
|
+
SQL
|
|
40
|
+
# ADVERSARIAL FINDING: a destination whose top segment already
|
|
41
|
+
# holds a value — most commonly a reference, a bare scalar id
|
|
42
|
+
# — used to be silently overwritten with an empty object the
|
|
43
|
+
# moment a dotted destination needed to nest under it. That is
|
|
44
|
+
# a drop that never declared itself, the one thing this
|
|
45
|
+
# language exists to make explicit (see hecks_tr_convert's own
|
|
46
|
+
# refusal below, the same shape) — refused here instead, with
|
|
47
|
+
# the Ruby reference transform (ports/persistence/lineage.rb's
|
|
48
|
+
# `insert`) raising the identical wording.
|
|
49
|
+
@db.exec(<<~SQL)
|
|
50
|
+
CREATE OR REPLACE FUNCTION hecks_tr_insert(state jsonb, path text[], value jsonb, rule_label text) RETURNS jsonb
|
|
51
|
+
LANGUAGE plpgsql IMMUTABLE AS $fn$
|
|
52
|
+
BEGIN
|
|
53
|
+
IF array_length(path, 1) = 1 THEN
|
|
54
|
+
RETURN state || jsonb_build_object(path[1], value);
|
|
55
|
+
END IF;
|
|
56
|
+
IF state ? path[1] AND jsonb_typeof(state -> path[1]) <> 'object' THEN
|
|
57
|
+
RAISE EXCEPTION 'cannot %: % already holds %, not a value this can nest under — moving into it would discard that value silently. Rename or drop % first.',
|
|
58
|
+
rule_label, path[1], state -> path[1], path[1];
|
|
59
|
+
END IF;
|
|
60
|
+
IF state -> path[1] IS NULL THEN
|
|
61
|
+
state := state || jsonb_build_object(path[1], '{}'::jsonb);
|
|
62
|
+
END IF;
|
|
63
|
+
RETURN jsonb_set(state, path, value);
|
|
64
|
+
END $fn$
|
|
65
|
+
SQL
|
|
66
|
+
@db.exec(<<~SQL)
|
|
67
|
+
CREATE OR REPLACE FUNCTION hecks_tr_rename(state jsonb, old_name text, new_name text) RETURNS jsonb
|
|
68
|
+
LANGUAGE sql IMMUTABLE AS $fn$
|
|
69
|
+
SELECT CASE WHEN state ? old_name
|
|
70
|
+
THEN (state - old_name) || jsonb_build_object(new_name, state -> old_name)
|
|
71
|
+
ELSE state END
|
|
72
|
+
$fn$
|
|
73
|
+
SQL
|
|
74
|
+
@db.exec(<<~SQL)
|
|
75
|
+
CREATE OR REPLACE FUNCTION hecks_tr_move(state jsonb, from_path text[], to_path text[], rule_label text) RETURNS jsonb
|
|
76
|
+
LANGUAGE plpgsql IMMUTABLE AS $fn$
|
|
77
|
+
DECLARE extracted record;
|
|
78
|
+
BEGIN
|
|
79
|
+
SELECT * INTO extracted FROM hecks_tr_extract(state, from_path);
|
|
80
|
+
IF NOT extracted.present THEN RETURN state; END IF;
|
|
81
|
+
RETURN hecks_tr_insert(extracted.remaining, to_path, extracted.value, rule_label);
|
|
82
|
+
END $fn$
|
|
83
|
+
SQL
|
|
84
|
+
@db.exec(<<~SQL)
|
|
85
|
+
CREATE OR REPLACE FUNCTION hecks_tr_convert(state jsonb, from_path text[], to_path text[], pairs jsonb, from_label text, rule_label text) RETURNS jsonb
|
|
86
|
+
LANGUAGE plpgsql IMMUTABLE AS $fn$
|
|
87
|
+
DECLARE extracted record; pair jsonb;
|
|
88
|
+
BEGIN
|
|
89
|
+
SELECT * INTO extracted FROM hecks_tr_extract(state, from_path);
|
|
90
|
+
IF NOT extracted.present THEN RETURN state; END IF;
|
|
91
|
+
FOR pair IN SELECT * FROM jsonb_array_elements(pairs) LOOP
|
|
92
|
+
IF pair -> 0 = extracted.value THEN
|
|
93
|
+
RETURN hecks_tr_insert(extracted.remaining, to_path, pair -> 1, rule_label);
|
|
94
|
+
END IF;
|
|
95
|
+
END LOOP;
|
|
96
|
+
RAISE EXCEPTION 'cannot translate %: % has no mapping in its convert''s values: table. Add % => ... to cover it.',
|
|
97
|
+
from_label, extracted.value, extracted.value;
|
|
98
|
+
END $fn$
|
|
99
|
+
SQL
|
|
100
|
+
@db.exec(<<~SQL)
|
|
101
|
+
CREATE OR REPLACE FUNCTION hecks_tr_drop(state jsonb, path text[]) RETURNS jsonb
|
|
102
|
+
LANGUAGE plpgsql IMMUTABLE AS $fn$
|
|
103
|
+
DECLARE extracted record;
|
|
104
|
+
BEGIN
|
|
105
|
+
SELECT * INTO extracted FROM hecks_tr_extract(state, path);
|
|
106
|
+
RETURN extracted.remaining;
|
|
107
|
+
END $fn$
|
|
108
|
+
SQL
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "digest"
|
|
3
|
+
|
|
4
|
+
require_relative "lineage/provisioning"
|
|
5
|
+
require_relative "lineage/era_store"
|
|
6
|
+
require_relative "lineage/mint_transaction"
|
|
7
|
+
require_relative "lineage/tail_merge"
|
|
8
|
+
require_relative "lineage/resumable_backfill"
|
|
9
|
+
require_relative "lineage/head_compiler"
|
|
10
|
+
require_relative "lineage/field_cache"
|
|
11
|
+
require_relative "lineage/transform_installer"
|
|
12
|
+
require_relative "../../../naming"
|
|
13
|
+
|
|
14
|
+
module Hecks
|
|
15
|
+
module Adapters
|
|
16
|
+
class PostgresEra
|
|
17
|
+
# The lineage topology inside one Postgres database: one journal
|
|
18
|
+
# per domain, LIST-partitioned by era, with a single ordinal
|
|
19
|
+
# sequence spanning partitions (total order across eras is
|
|
20
|
+
# structural); a `hecks_eras` table holding each era's frozen
|
|
21
|
+
# source text, its once-minted hash/label, and the watermark cut
|
|
22
|
+
# into its ancestor; and, per aggregate, a HEAD derived from the
|
|
23
|
+
# journal — never a table anything rewrites.
|
|
24
|
+
#
|
|
25
|
+
# For era 1 the head is a plain view (latest save per id). From
|
|
26
|
+
# era 2 on, the ancestor tail is a MATERIALIZED view whose
|
|
27
|
+
# definition is the compiled, chained edge sequence — one CTE per
|
|
28
|
+
# original edge, in mint order, never a flattened merged rule set
|
|
29
|
+
# — with the watermark baked into the definition, so post-cut
|
|
30
|
+
# old-era writes cannot leak into the new head even on REFRESH.
|
|
31
|
+
# Live current-era writes overlay it through the head view.
|
|
32
|
+
#
|
|
33
|
+
# Writing to a superseded schema drops the instant the new one
|
|
34
|
+
# materializes: ONE shared row policy admits INSERTs to whichever
|
|
35
|
+
# era was just established (era 1 at first hold, era N at mint),
|
|
36
|
+
# and advancing it is part of the SAME transaction that builds the
|
|
37
|
+
# new era's matview. There is no persisted per-role fork — any
|
|
38
|
+
# granted role, app or the table's own OWNER (FORCE ROW LEVEL
|
|
39
|
+
# SECURITY applies this to the owner too, not only ordinary
|
|
40
|
+
# roles), writes the current era or nothing, from the moment that
|
|
41
|
+
# transaction commits. A stale-era write during the narrow window
|
|
42
|
+
# before that commit — RLS is checked once, when the statement
|
|
43
|
+
# EXECUTES, never re-checked at commit, so a transaction that
|
|
44
|
+
# inserted while the old era was still current can still land
|
|
45
|
+
# after a concurrent mint has already moved the fence on — is
|
|
46
|
+
# exactly what diverged_count/merge_tail exist to reconcile; it is
|
|
47
|
+
# the residual of an unavoidable race (ordinals are
|
|
48
|
+
# sequence-assigned, not transactional — see below), not a
|
|
49
|
+
# supported way to keep operating two schemas side by side. Only
|
|
50
|
+
# an actual Postgres superuser (or a role granted BYPASSRLS)
|
|
51
|
+
# sits above FORCE and keeps writing at will, forever.
|
|
52
|
+
#
|
|
53
|
+
# Lineage order is ordinal-assignment order: the ordinal comes from a
|
|
54
|
+
# sequence, and a sequence's `nextval()` is never rolled back with its
|
|
55
|
+
# transaction — accepted and documented rather than papered over.
|
|
56
|
+
#
|
|
57
|
+
# ONE PART OF THAT IS CLOSED. Two concurrent PLAIN writes could call
|
|
58
|
+
# `nextval()` in one order and COMMIT in the other — nothing about a
|
|
59
|
+
# single autocommit INSERT statement stops a slower one from finishing
|
|
60
|
+
# after a faster one that started later — so "ordinal order" and
|
|
61
|
+
# "commit order" were formally two different total orders even with no
|
|
62
|
+
# mint anywhere near either write. `PostgresEra#append` now holds
|
|
63
|
+
# `pg_advisory_xact_lock(hashtext('hecks_ordinal:' || domain))` for the
|
|
64
|
+
# length of its own transaction, a DIFFERENT key from `mint_era!` and
|
|
65
|
+
# `merge_tail!`'s `hecks_eras:domain` — so plain writes serialize
|
|
66
|
+
# against EACH OTHER only, never against a mint, and ordinal order
|
|
67
|
+
# equals commit order for them now.
|
|
68
|
+
#
|
|
69
|
+
# THE OTHER PART IS NOT, ON PURPOSE. A stale-era write during the
|
|
70
|
+
# narrow window before a mint's fence-move commits is the SAME race by
|
|
71
|
+
# a different name — and closing it would mean a plain write
|
|
72
|
+
# serializing against a mint, which is exactly the guarantee
|
|
73
|
+
# `postgres_lineage_spec.rb`'s "an old checkout keeps writing its own
|
|
74
|
+
# era THROUGH a mint" pins the ABSENCE of. That race stays the
|
|
75
|
+
# residual `diverged_count`/`merge_tail!` exist to reconcile, not
|
|
76
|
+
# something a plain write should ever block for.
|
|
77
|
+
#
|
|
78
|
+
# One concern per file under lineage/: provisioning (DDL and the
|
|
79
|
+
# RLS posture), era_store (the hecks_eras rows and their integrity),
|
|
80
|
+
# mint_transaction (the one transaction that makes an era real),
|
|
81
|
+
# tail_merge (the one deliberate merge command), resumable_backfill
|
|
82
|
+
# (the one chunked/lock-free/resumable scan loop, shared by
|
|
83
|
+
# head_compiler's own backfill and field_cache's), head_compiler
|
|
84
|
+
# (the chained-edge SQL a head derives through), field_cache (the
|
|
85
|
+
# per-where-field read cache that lets a query skip the reduction
|
|
86
|
+
# entirely), transform_installer (the hecks_tr_* jsonb helpers).
|
|
87
|
+
class Lineage
|
|
88
|
+
include Provisioning
|
|
89
|
+
include EraStore
|
|
90
|
+
include MintTransaction
|
|
91
|
+
include TailMerge
|
|
92
|
+
include ResumableBackfill
|
|
93
|
+
include HeadCompiler
|
|
94
|
+
include FieldCache
|
|
95
|
+
include TransformInstaller
|
|
96
|
+
|
|
97
|
+
JOURNAL_COLUMNS = "ordinal, era, aggregate, aggregate_id, operation, state, mirrors".freeze
|
|
98
|
+
|
|
99
|
+
attr_reader :db, :domain, :formerly_known_as
|
|
100
|
+
|
|
101
|
+
def initialize(db, domain, formerly_known_as: nil)
|
|
102
|
+
@db = db
|
|
103
|
+
@domain = domain.to_s
|
|
104
|
+
@formerly_known_as = formerly_known_as&.to_s
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def journal = "hecks_journal_#{Naming.snake(@domain)}"
|
|
108
|
+
def quoted_journal = quote(journal)
|
|
109
|
+
def sequence = "#{journal}_ordinal"
|
|
110
|
+
def partition(era) = "#{journal}_era_#{era}"
|
|
111
|
+
def head_view(storage_name) = "#{storage_name}_head"
|
|
112
|
+
# The transactionally-upserted read cache behind head_view — one row
|
|
113
|
+
# per LIVE id, keyed by id, carrying the ordinal it was last written
|
|
114
|
+
# at. Scoped by ERA, not just storage_name — an aggregate that
|
|
115
|
+
# ISN'T renamed across a mint keeps the SAME storage_name in both
|
|
116
|
+
# eras, so storage_name alone would have era N+1 sharing one
|
|
117
|
+
# physical table with era N: a freshly-minted era would inherit
|
|
118
|
+
# every pre-mint (and, worse, pre-rekey/pre-translation) row
|
|
119
|
+
# instead of starting empty. era-qualified naming is what
|
|
120
|
+
# `partition`/`matview` already do for exactly this reason.
|
|
121
|
+
def head_snapshot(storage_name, era) = "#{storage_name}_head_snapshot_#{era}"
|
|
122
|
+
def matview(storage_name, era, label) = "#{storage_name}_lineage_#{era}_#{label}"
|
|
123
|
+
|
|
124
|
+
private
|
|
125
|
+
|
|
126
|
+
def quote(name) = PG::Connection.quote_ident(name.to_s)
|
|
127
|
+
|
|
128
|
+
def text_literal(text) = "'#{text.to_s.gsub("'", "''")}'"
|
|
129
|
+
|
|
130
|
+
def path_literal(path)
|
|
131
|
+
segments = path.to_s.split(".").map { |segment| text_literal(segment) }
|
|
132
|
+
"ARRAY[#{segments.join(', ')}]::text[]"
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
require_relative "../../../../ports/persistence/lineage"
|
|
2
|
+
require_relative "../../../../runtime/era_guard"
|
|
3
|
+
require_relative "../../../../runtime/identity"
|
|
4
|
+
require_relative "../../../../runtime/registry"
|
|
5
|
+
require_relative "../../../../translation/audit"
|
|
6
|
+
|
|
7
|
+
module Hecks
|
|
8
|
+
module Adapters
|
|
9
|
+
class PostgresEra
|
|
10
|
+
module LineageManager
|
|
11
|
+
# What a mint must prove before it may happen: the edge covers
|
|
12
|
+
# the whole diff, no identity path was re-keyed, and the audit's
|
|
13
|
+
# first two layers pass over the LIVE compiled chain.
|
|
14
|
+
module CoverageCheck
|
|
15
|
+
# Layer 1 against THIS edge specifically: every vanished or
|
|
16
|
+
# retyped path in the held→current diff must be explained, and
|
|
17
|
+
# every held aggregate must still be claimed. The refusal is
|
|
18
|
+
# EraGuard's own, byte for byte.
|
|
19
|
+
def check_coverage!(registry, bluebook, held_bluebook, edge)
|
|
20
|
+
bluebook.aggregates.each do |aggregate|
|
|
21
|
+
rules = Ports::Persistence::Lineage.from_declared(edge.for_aggregate(aggregate.name), aggregate.name)
|
|
22
|
+
held_aggregate = held_bluebook.aggregate(rules&.ancestor_name || aggregate.name)
|
|
23
|
+
next unless held_aggregate
|
|
24
|
+
|
|
25
|
+
check_identity_unchanged!(bluebook, aggregate, held_aggregate, rules)
|
|
26
|
+
uncovered = Runtime::EraGuard.uncovered_attributes(aggregate, held_aggregate, rules)
|
|
27
|
+
Runtime::EraGuard.refuse_uncovered!(bluebook, aggregate, uncovered) unless uncovered.empty?
|
|
28
|
+
|
|
29
|
+
unsafe = Runtime::EraGuard.unsafe_additions(aggregate, held_aggregate, rules)
|
|
30
|
+
Runtime::EraGuard.refuse_unsafe_addition!(bluebook, aggregate, unsafe) unless unsafe.empty?
|
|
31
|
+
end
|
|
32
|
+
Runtime::EraGuard.check_vanished_aggregates!(registry, bluebook, held_bluebook)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# An identity-path change is a RE-KEYING, not an ordinary
|
|
36
|
+
# translation — stored ids were fixed at write time under the old
|
|
37
|
+
# key, so this refuses UNLESS the edge declares a `rekey` for
|
|
38
|
+
# this aggregate covering exactly that. `rules.rekey?` (see
|
|
39
|
+
# `Ports::Persistence::Lineage`'s own comment) is the single
|
|
40
|
+
# source of truth every consumer of this fact asks — this is not
|
|
41
|
+
# a second, independent check of `declared.rekeys`.
|
|
42
|
+
def check_identity_unchanged!(bluebook, aggregate, held_aggregate, rules)
|
|
43
|
+
# The FULL declared path lists, in declaration order — never the
|
|
44
|
+
# single-head shortcut, which is nil for every composite identity
|
|
45
|
+
# and so would let two different composites compare as unchanged.
|
|
46
|
+
return if held_aggregate.identity_paths == aggregate.identity_paths
|
|
47
|
+
return if rules&.rekey?
|
|
48
|
+
|
|
49
|
+
held_identity = Runtime::Identity.reading(held_aggregate)
|
|
50
|
+
current_identity = Runtime::Identity.reading(aggregate)
|
|
51
|
+
raise Runtime::WiringError,
|
|
52
|
+
"cannot mint an era for #{bluebook.name}::#{aggregate.name}: its identity path changed " \
|
|
53
|
+
"(#{held_identity} → #{current_identity}), and that is a re-keying, not a translation — " \
|
|
54
|
+
"stored ids were minted under #{held_identity}, and no rule declares rows the same " \
|
|
55
|
+
"entity under a new key. Keep the identity path, declare a rekey rule, or migrate the " \
|
|
56
|
+
"data explicitly"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Layers 1 and 2 of the audit, over the LIVE compiled chain —
|
|
60
|
+
# before anything is minted, so a refusal leaves no half-born
|
|
61
|
+
# era. (A convert meeting an unmapped value raises inside the
|
|
62
|
+
# preview query itself: same rollback-shaped outcome.)
|
|
63
|
+
def audit!(bluebook, lineage, chain, ordinal, edge)
|
|
64
|
+
violations = []
|
|
65
|
+
bluebook.aggregates.each do |aggregate|
|
|
66
|
+
declared = edge.for_aggregate(aggregate.name)
|
|
67
|
+
after = begin
|
|
68
|
+
lineage.translated_latest(aggregate, ordinal, chain)
|
|
69
|
+
rescue PG::Error => error
|
|
70
|
+
raise Runtime::WiringError, "cannot mint era #{ordinal} of #{bluebook.name}: #{error.message.strip}"
|
|
71
|
+
end
|
|
72
|
+
before = if chain.size > 1
|
|
73
|
+
lineage.translated_latest(aggregate, ordinal, chain[0..-2])
|
|
74
|
+
else
|
|
75
|
+
lineage.ancestor_latest(aggregate, ordinal, chain)
|
|
76
|
+
end
|
|
77
|
+
verdict = Translation::Audit.check(aggregate: aggregate, declared: declared, before: before, after: after)
|
|
78
|
+
violations.concat(verdict.violations)
|
|
79
|
+
end
|
|
80
|
+
return if violations.empty?
|
|
81
|
+
|
|
82
|
+
raise Runtime::WiringError,
|
|
83
|
+
"cannot mint era #{ordinal} of #{bluebook.name}: the audit refused —\n - #{violations.join("\n - ")}"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
require_relative "../lineage"
|
|
2
|
+
require_relative "../../../../runtime/storage_shape"
|
|
3
|
+
|
|
4
|
+
module Hecks
|
|
5
|
+
module Adapters
|
|
6
|
+
class PostgresEra
|
|
7
|
+
module LineageManager
|
|
8
|
+
# The boot-time resolution: which era IS this checkout? First
|
|
9
|
+
# boot holds era 1; a quiet reboot changes nothing; a
|
|
10
|
+
# held-but-superseded shape boots read-only-toward-the-fence; an
|
|
11
|
+
# unheld shape goes to the minter.
|
|
12
|
+
module EraResolver
|
|
13
|
+
def check!(registry:, bluebook:, current_text:, settings:, directory: nil)
|
|
14
|
+
db = PostgresEra.connect_for(bluebook.name, settings)
|
|
15
|
+
lineage = Lineage.new(db, bluebook.name, formerly_known_as: bluebook.formerly_known_as)
|
|
16
|
+
lineage.ensure_base!
|
|
17
|
+
role = settings[:role] || settings["role"]
|
|
18
|
+
|
|
19
|
+
held = lineage.eras
|
|
20
|
+
if held.empty?
|
|
21
|
+
lineage.hold_first!(current_text, projection: Runtime::StorageShape.project(bluebook))
|
|
22
|
+
bluebook.aggregates.each { |aggregate| lineage.ensure_first_head!(aggregate.storage_name) }
|
|
23
|
+
# hold_first! already established era 1 as current for
|
|
24
|
+
# EVERY role; this one just needs its own privileges.
|
|
25
|
+
lineage.grant_role!(role, aggregates: bluebook.aggregates, era: 1) if role
|
|
26
|
+
return
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
current_shape = Runtime::StorageShape.project(bluebook)
|
|
30
|
+
shapes = held.map { |era| [era, Runtime::StorageShape.project(shadow(era[:held_text]))] }
|
|
31
|
+
if bluebook.formerly_known_as
|
|
32
|
+
# A held era's shape is re-derived from its own frozen
|
|
33
|
+
# historical text (shadow(era[:held_text])), so its "name"
|
|
34
|
+
# is permanently the old one — the frozen text is authentic
|
|
35
|
+
# record ("this domain really was called that, at the
|
|
36
|
+
# time") and must never be rewritten. Only the in-memory
|
|
37
|
+
# shape used for THIS comparison is normalized, and only
|
|
38
|
+
# where it exactly matches the declared old name, so an
|
|
39
|
+
# unrelated domain that happens to shape-match some other
|
|
40
|
+
# domain's history still shows up as a real mismatch.
|
|
41
|
+
shapes = shapes.map do |era, shape|
|
|
42
|
+
[era, shape["name"] == bluebook.formerly_known_as ? shape.merge("name" => bluebook.name) : shape]
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
latest, latest_shape = shapes.last
|
|
47
|
+
if latest_shape == current_shape
|
|
48
|
+
bluebook.aggregates.each { |aggregate| lineage.ensure_first_head!(aggregate.storage_name) } if latest[:ordinal] == 1
|
|
49
|
+
# A quiet reboot changes no era, so there is nothing to
|
|
50
|
+
# advance — only this role's own privileges, if it is new.
|
|
51
|
+
lineage.grant_role!(role, aggregates: bluebook.aggregates, era: latest[:ordinal]) if role
|
|
52
|
+
registry.resolved_eras[bluebook.name] = latest[:ordinal]
|
|
53
|
+
return
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
matched, = shapes.find { |_, shape| shape == current_shape }
|
|
57
|
+
if matched
|
|
58
|
+
# A held-but-superseded era — an old checkout still running.
|
|
59
|
+
# It may keep BOOTING and READING (PostgresEra is the one
|
|
60
|
+
# adapter that recognizes this rather than refusing), but it
|
|
61
|
+
# may not keep WRITING: the shared era fence was already
|
|
62
|
+
# advanced past this ordinal by whichever mint superseded
|
|
63
|
+
# it, and nothing here may roll that back. Granting only
|
|
64
|
+
# this role's privileges, never advance_era!, is what keeps
|
|
65
|
+
# that true — see advance_era!'s own warning against being
|
|
66
|
+
# called with a superseded ordinal.
|
|
67
|
+
lineage.grant_role!(role, aggregates: bluebook.aggregates, era: matched[:ordinal]) if role
|
|
68
|
+
registry.resolved_eras[bluebook.name] = matched[:ordinal]
|
|
69
|
+
return
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
registry.resolved_eras[bluebook.name] =
|
|
73
|
+
mint!(registry, bluebook, current_text, lineage, latest,
|
|
74
|
+
role: role, directory: directory)
|
|
75
|
+
ensure
|
|
76
|
+
db&.close
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
require_relative "../lineage"
|
|
2
|
+
require_relative "../../../../runtime/registry"
|
|
3
|
+
require_relative "../../../../translation/audit"
|
|
4
|
+
|
|
5
|
+
module Hecks
|
|
6
|
+
module Adapters
|
|
7
|
+
class PostgresEra
|
|
8
|
+
module LineageManager
|
|
9
|
+
# Tail-merge, driven from bin/merge_tail: interleave the old
|
|
10
|
+
# world's post-cut writes into the head by their recorded global
|
|
11
|
+
# ordinals, under the full audit, in one transaction.
|
|
12
|
+
module MergeCoordinator
|
|
13
|
+
def merge!(registry:, bluebook:, settings:, winners: {})
|
|
14
|
+
db = PostgresEra.connect_for(bluebook.name, settings)
|
|
15
|
+
lineage = Lineage.new(db, bluebook.name, formerly_known_as: bluebook.formerly_known_as)
|
|
16
|
+
lineage.ensure_base!
|
|
17
|
+
|
|
18
|
+
held = lineage.eras
|
|
19
|
+
raise Runtime::WiringError, "nothing to merge — #{bluebook.name} stands at era 1" if held.size < 2
|
|
20
|
+
|
|
21
|
+
latest = held.last
|
|
22
|
+
chain = edge_chain(registry, bluebook, held[0..-2], latest[:label])
|
|
23
|
+
|
|
24
|
+
audit = lambda do
|
|
25
|
+
violations = []
|
|
26
|
+
bluebook.aggregates.each do |aggregate|
|
|
27
|
+
rows = db.exec("SELECT id, state FROM #{PG::Connection.quote_ident(lineage.head_view(aggregate.storage_name))}")
|
|
28
|
+
.to_h { |row| [row["id"], JSON.parse(row["state"])] }
|
|
29
|
+
verdict = Translation::Audit.check(aggregate: aggregate, declared: nil, before: rows, after: rows)
|
|
30
|
+
violations.concat(verdict.violations)
|
|
31
|
+
end
|
|
32
|
+
violations
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
lineage.merge_tail!(aggregates: bluebook.aggregates, edges: chain, winners: winners, audit: audit)
|
|
36
|
+
ensure
|
|
37
|
+
db&.close
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|