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,391 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
require_relative "sql_query_builder"
|
|
4
|
+
require_relative "postgres/schema_builder"
|
|
5
|
+
require_relative "postgres/codec"
|
|
6
|
+
require_relative "../../ports/persistence/append_only"
|
|
7
|
+
require_relative "../../query_specification/common/null_policy"
|
|
8
|
+
require_relative "../../query_specification/common/order_by"
|
|
9
|
+
require_relative "../../query_specification/field_path"
|
|
10
|
+
require_relative "../../runtime/errors"
|
|
11
|
+
require_relative "../../runtime/event"
|
|
12
|
+
require_relative "../../runtime/instance"
|
|
13
|
+
|
|
14
|
+
module Hecks
|
|
15
|
+
module Adapters
|
|
16
|
+
# The plain Postgres store — flat, one table per aggregate, real
|
|
17
|
+
# typed columns for scalars and jsonb for nested/list attributes,
|
|
18
|
+
# exactly the shape `Sqlite` already made for its own table. Sibling
|
|
19
|
+
# to `PostgresEra` (postgres_era.rb), which is the same database with
|
|
20
|
+
# full lineage/era machinery on top — pick this one unless a domain
|
|
21
|
+
# actually needs to survive a live shape change. See
|
|
22
|
+
# docs/implemented/postgres-era-adapter-split-plan.md for why the two are split.
|
|
23
|
+
#
|
|
24
|
+
# No `hecks_eras`, no lineage, no advisory-lock-per-write for era
|
|
25
|
+
# tracking, no `lineage_capable?`/`era_check!` — this class simply
|
|
26
|
+
# doesn't define those methods at all, and the capability idiom
|
|
27
|
+
# elsewhere already treats their absence as "not lineage-capable".
|
|
28
|
+
#
|
|
29
|
+
# Storage shape (see postgres/schema_builder.rb for the DDL,
|
|
30
|
+
# postgres/codec.rb for the encode/decode):
|
|
31
|
+
# - One real column per attribute, typed for a scalar
|
|
32
|
+
# (`SQL_TYPES`, `text` default), `jsonb` for a nested (value-object)
|
|
33
|
+
# or list-typed attribute — never JSON-in-TEXT the way `Sqlite` has
|
|
34
|
+
# to, since Postgres has a native jsonb type.
|
|
35
|
+
# - `append` and `project` are two real Postgres statements — `save`/
|
|
36
|
+
# `delete` wrap them in ONE transaction, same "the journal insert
|
|
37
|
+
# and the snapshot stay atomic" reasoning `PostgresEra#append`'s own
|
|
38
|
+
# comment gives: a crash between the two must never leave a
|
|
39
|
+
# half-written state. `append`/`project` stay plain, individually-
|
|
40
|
+
# callable methods (never wrapping their own transaction) so
|
|
41
|
+
# `AppendOnly#recover!`'s replay — `project` alone, no `append` —
|
|
42
|
+
# keeps working the same way it does on every other adapter.
|
|
43
|
+
class Postgres
|
|
44
|
+
include SqlQueryBuilder
|
|
45
|
+
include SchemaBuilder
|
|
46
|
+
include Codec
|
|
47
|
+
|
|
48
|
+
SQL_TYPES = { "Integer" => "bigint", "Float" => "double precision" }.freeze
|
|
49
|
+
|
|
50
|
+
attr_reader :aggregate
|
|
51
|
+
|
|
52
|
+
def persistence_capabilities = [:atomic_put]
|
|
53
|
+
|
|
54
|
+
def self.connect_for(name, settings)
|
|
55
|
+
# LAZY, ON PURPOSE — same reasoning as PostgresEra's own
|
|
56
|
+
# connect_for: a domain that never wires Postgres should never
|
|
57
|
+
# need the gem installed.
|
|
58
|
+
require "pg"
|
|
59
|
+
|
|
60
|
+
declared = settings[:database] || settings["database"]
|
|
61
|
+
if declared.to_s.empty?
|
|
62
|
+
raise Runtime::WiringError,
|
|
63
|
+
"#{name} binds Postgres, which needs a database connection, " \
|
|
64
|
+
"but its world declares no \"database\"."
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
connection =
|
|
68
|
+
if declared.start_with?("postgres://", "postgresql://")
|
|
69
|
+
PG.connect(declared)
|
|
70
|
+
else
|
|
71
|
+
PG.connect(dbname: declared)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# SHARED-INSTANCE ISOLATION — same as PostgresEra's own: a
|
|
75
|
+
# domain that declares `schema` is sharing its Postgres instance
|
|
76
|
+
# with other domains, so every unqualified reference this
|
|
77
|
+
# adapter constructs resolves through search_path. A domain with
|
|
78
|
+
# no `schema` setting keeps Postgres's own default (public).
|
|
79
|
+
schema = settings[:schema] || settings["schema"]
|
|
80
|
+
connection.exec("SET search_path TO #{connection.quote_ident(schema)}") if schema.to_s != ""
|
|
81
|
+
|
|
82
|
+
# QUIET ON PURPOSE — same reasoning as PostgresEra's own: a
|
|
83
|
+
# schema/table that already exists is the ORDINARY case on every
|
|
84
|
+
# boot after the first, not news.
|
|
85
|
+
connection.exec("SET client_min_messages = warning")
|
|
86
|
+
connection
|
|
87
|
+
rescue PG::Error => error
|
|
88
|
+
raise Runtime::WiringError,
|
|
89
|
+
"cannot bind Postgres at #{declared} for #{name}: #{error.message.strip}"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def initialize(aggregate:, settings: {}, root: nil)
|
|
93
|
+
@aggregate = aggregate
|
|
94
|
+
@db = self.class.connect_for(aggregate.name, settings)
|
|
95
|
+
# THE OPTIONAL saga-persistence capability's own scoping column
|
|
96
|
+
# (§2/§4) — falls back to the aggregate's own storage name for a
|
|
97
|
+
# directly-instantiated adapter (specs), same fallback shape
|
|
98
|
+
# Sqlite's own @domain already uses.
|
|
99
|
+
@domain = (settings[:domain] || settings["domain"] || aggregate.storage_name).to_s
|
|
100
|
+
|
|
101
|
+
create_aggregate_table!
|
|
102
|
+
create_entry_table!
|
|
103
|
+
create_event_table!
|
|
104
|
+
create_saga_table!
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def table = @aggregate.storage_name
|
|
108
|
+
|
|
109
|
+
def find(id)
|
|
110
|
+
result = @db.exec_params("SELECT * FROM #{quoted_table} WHERE id = $1", [id.to_s])
|
|
111
|
+
return nil if result.ntuples.zero?
|
|
112
|
+
|
|
113
|
+
Runtime::Instance.new(aggregate: @aggregate, id: result[0]["id"], state: decode(result[0]))
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# order_by IS A RUNTIME VALUE — see Sqlite#all's own reasoning;
|
|
117
|
+
# whitelisted the identical way before it ever reaches
|
|
118
|
+
# order_expression.
|
|
119
|
+
def all(order_by: nil, direction: :asc)
|
|
120
|
+
order_sql = "ORDER BY id"
|
|
121
|
+
if order_by
|
|
122
|
+
name = order_by.to_s.split(".").first
|
|
123
|
+
raise Runtime::WiringError, "#{@aggregate.name} has no attribute #{order_by.inspect} to order by" unless @aggregate.lifecycle&.field.to_s == name || @aggregate.attribute(name)
|
|
124
|
+
|
|
125
|
+
spec = QuerySpecification::Common::OrderBy.new(field: order_by, direction: direction)
|
|
126
|
+
order_sql = "ORDER BY #{order_clause(spec, nil)}"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
@db.exec("SELECT * FROM #{quoted_table} #{order_sql}").map do |row|
|
|
130
|
+
Runtime::Instance.new(aggregate: @aggregate, id: row["id"], state: decode(row))
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def count = @db.exec("SELECT COUNT(*) FROM #{quoted_table}")[0]["count"].to_i
|
|
135
|
+
|
|
136
|
+
def append(entry)
|
|
137
|
+
@db.exec_params(
|
|
138
|
+
"INSERT INTO #{quoted_entry_table} (aggregate_id, operation, state, mirrors) VALUES ($1, $2, $3, $4)",
|
|
139
|
+
[entry.id, entry.operation, JSON.generate(entry.state), JSON.generate(entry.mirrors)]
|
|
140
|
+
)
|
|
141
|
+
entry
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def project(entry)
|
|
145
|
+
return @db.exec_params("DELETE FROM #{quoted_table} WHERE id = $1", [entry.id]) if entry.delete?
|
|
146
|
+
|
|
147
|
+
instance = Runtime::Instance.new(aggregate: @aggregate, id: entry.id, state: entry.state)
|
|
148
|
+
columns = (["id"] + persisted_fields.map { |field| field[:name].to_s })
|
|
149
|
+
values = [instance.id.to_s] + persisted_fields.map { |field| encode_field(field, instance[field[:name]]) }
|
|
150
|
+
updates = persisted_fields.map { |field| "#{quote_ident(field[:name])} = EXCLUDED.#{quote_ident(field[:name])}" }
|
|
151
|
+
|
|
152
|
+
@db.exec_params(
|
|
153
|
+
"INSERT INTO #{quoted_table} (#{columns.map { |c| quote_ident(c) }.join(', ')}) " \
|
|
154
|
+
"VALUES (#{(1..columns.size).map { |n| "$#{n}" }.join(', ')}) " \
|
|
155
|
+
"ON CONFLICT (id) DO UPDATE SET #{updates.join(', ')}",
|
|
156
|
+
values
|
|
157
|
+
)
|
|
158
|
+
instance
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def entries
|
|
162
|
+
@db.exec("SELECT aggregate_id, operation, state, mirrors FROM #{quoted_entry_table} ORDER BY sequence").map do |row|
|
|
163
|
+
state = JSON.parse(row["state"])
|
|
164
|
+
Ports::Persistence::Entry.new(
|
|
165
|
+
operation: row["operation"] || "save",
|
|
166
|
+
id: row["aggregate_id"],
|
|
167
|
+
state: state&.transform_keys(&:to_sym),
|
|
168
|
+
mirrors: row["mirrors"] && JSON.parse(row["mirrors"])
|
|
169
|
+
)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def reset!
|
|
174
|
+
@db.exec("DELETE FROM #{quoted_table}")
|
|
175
|
+
@db.exec("DELETE FROM #{quoted_entry_table}")
|
|
176
|
+
self
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# ONE TRANSACTION, not the plain append-then-project two-step a
|
|
180
|
+
# file-based adapter needs a crash-recovery replay for (Heki) —
|
|
181
|
+
# real Postgres ACID atomicity is sitting right there, so a crash
|
|
182
|
+
# between the journal insert and the table upsert must not leave
|
|
183
|
+
# the two disagreeing. `append`/`project` themselves stay plain,
|
|
184
|
+
# transaction-free methods (see the class comment above) — the
|
|
185
|
+
# transaction lives here, the one caller that runs both together.
|
|
186
|
+
def save(instance)
|
|
187
|
+
entry = Ports::Persistence::Entry.new(operation: "save", id: instance.id.to_s, state: instance.state.dup)
|
|
188
|
+
@db.transaction { append(entry); project(entry) }
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Classification, journal append and snapshot replacement are one
|
|
192
|
+
# Postgres transaction. A row lock cannot serialize two first writers —
|
|
193
|
+
# there is no row to lock yet — so a transaction-scoped advisory lock on
|
|
194
|
+
# schema/table + aggregate id owns that missing-row race as well. Once a
|
|
195
|
+
# writer acquires it, the preceding writer has committed and status can
|
|
196
|
+
# be read from the materialized table without a runtime-side find.
|
|
197
|
+
def atomic_put(entry, insert_only: false)
|
|
198
|
+
status = nil
|
|
199
|
+
@db.transaction do
|
|
200
|
+
@db.exec_params(
|
|
201
|
+
"SELECT pg_advisory_xact_lock(" \
|
|
202
|
+
"hashtext(current_schema() || ':' || $1), hashtext($2))",
|
|
203
|
+
[table, entry.id.to_s]
|
|
204
|
+
)
|
|
205
|
+
exists = !@db.exec_params(
|
|
206
|
+
"SELECT 1 FROM #{quoted_table} WHERE id = $1",
|
|
207
|
+
[entry.id.to_s]
|
|
208
|
+
).ntuples.zero?
|
|
209
|
+
if insert_only && exists
|
|
210
|
+
status = :conflicted
|
|
211
|
+
next
|
|
212
|
+
end
|
|
213
|
+
status = exists ? :replaced : :inserted
|
|
214
|
+
append(entry)
|
|
215
|
+
project(entry)
|
|
216
|
+
end
|
|
217
|
+
status
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def delete(id)
|
|
221
|
+
entry = Ports::Persistence::Entry.new(operation: "delete", id: id.to_s, state: nil)
|
|
222
|
+
@db.transaction { append(entry); project(entry) }
|
|
223
|
+
true
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def record_event(event)
|
|
227
|
+
@db.exec_params(
|
|
228
|
+
"INSERT INTO events (name, aggregate, aggregate_id, payload, occurred_at) VALUES ($1, $2, $3, $4, $5)",
|
|
229
|
+
[event.name, event.aggregate, event.id.to_s, JSON.generate(event.payload), event.occurred_at]
|
|
230
|
+
)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def events
|
|
234
|
+
@db.exec("SELECT * FROM events ORDER BY id").map do |row|
|
|
235
|
+
Runtime::Event.new(
|
|
236
|
+
name: row["name"],
|
|
237
|
+
aggregate: row["aggregate"],
|
|
238
|
+
id: row["aggregate_id"],
|
|
239
|
+
payload: JSON.parse(row["payload"], symbolize_names: true),
|
|
240
|
+
occurred_at: row["occurred_at"]
|
|
241
|
+
)
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# ── the OPTIONAL saga-persistence capability (§2) — same DDL and
|
|
246
|
+
# shape as PostgresEra's own (postgres_era.rb), not lineage-
|
|
247
|
+
# specific, copied verbatim.
|
|
248
|
+
def save_saga(process_manager:, correlation:, state:, memory:)
|
|
249
|
+
@db.exec_params(
|
|
250
|
+
"INSERT INTO hecks_saga_instances (domain, process_manager, correlation, state, memory) " \
|
|
251
|
+
"VALUES ($1, $2, $3, $4, $5) " \
|
|
252
|
+
"ON CONFLICT (domain, process_manager, correlation) DO UPDATE " \
|
|
253
|
+
"SET state = EXCLUDED.state, memory = EXCLUDED.memory, updated_at = now()",
|
|
254
|
+
[@domain, process_manager.to_s, correlation.to_s, state.to_s, JSON.generate(memory)]
|
|
255
|
+
)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def delete_saga(process_manager:, correlation:)
|
|
259
|
+
@db.exec_params(
|
|
260
|
+
"DELETE FROM hecks_saga_instances WHERE domain = $1 AND process_manager = $2 AND correlation = $3",
|
|
261
|
+
[@domain, process_manager.to_s, correlation.to_s]
|
|
262
|
+
)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def each_saga
|
|
266
|
+
return enum_for(:each_saga) unless block_given?
|
|
267
|
+
|
|
268
|
+
@db.exec_params(
|
|
269
|
+
"SELECT process_manager, correlation, state, memory FROM hecks_saga_instances WHERE domain = $1",
|
|
270
|
+
[@domain]
|
|
271
|
+
).each do |row|
|
|
272
|
+
yield row["process_manager"], row["correlation"], row["state"],
|
|
273
|
+
JSON.parse(row["memory"], symbolize_names: true)
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
private
|
|
278
|
+
|
|
279
|
+
# ── SqlQueryBuilder's dialect hooks ─────────────────────────────
|
|
280
|
+
|
|
281
|
+
def select_list = "*"
|
|
282
|
+
def from_relation = quoted_table
|
|
283
|
+
def dialect_name = "Postgres"
|
|
284
|
+
def empty_in_clause = "FALSE"
|
|
285
|
+
|
|
286
|
+
def placeholder(binds, value)
|
|
287
|
+
binds << value
|
|
288
|
+
"$#{binds.size}"
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def contains_clause(expression, placeholder)
|
|
292
|
+
"position(#{placeholder} in #{expression}) > 0"
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# THE LIST COLUMN ITSELF IS THE JSONB ARRAY — no reaching into a
|
|
296
|
+
# shared blob a jsonb path has to walk into first (PostgresEra's
|
|
297
|
+
# own version does, since every attribute there shares one `state`
|
|
298
|
+
# column). Here, `column` names a real column of its own, already
|
|
299
|
+
# jsonb, already the array.
|
|
300
|
+
def list_contains_clause(column, member, placeholder)
|
|
301
|
+
target = member.empty? ? "elem #>> '{}'" : "elem ->> #{text_literal(member)}"
|
|
302
|
+
elements = "jsonb_array_elements(#{quote_ident(column)}) AS elem"
|
|
303
|
+
"EXISTS (SELECT 1 FROM #{elements} WHERE #{target} = #{placeholder})"
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def plain_column(name) = quote_ident(name)
|
|
307
|
+
|
|
308
|
+
# PostgresEra's own `jsonb_path` walks `[name, *path]` into ONE
|
|
309
|
+
# shared `state` column — the attribute name is PART of the path
|
|
310
|
+
# there. Here the attribute name IS THE COLUMN: the path into it
|
|
311
|
+
# is whatever is LEFT after the column, never the column name
|
|
312
|
+
# repeated inside its own path.
|
|
313
|
+
def nested_expression(name, path, member)
|
|
314
|
+
segments = path.empty? ? [(member || "value").to_s] : path
|
|
315
|
+
jsonb_path(name, segments)
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Scalar, non-value-object attributes get a REAL typed column
|
|
319
|
+
# (bigint/double precision/text) — comparing and sorting one needs
|
|
320
|
+
# no cast at all, unlike PostgresEra's shared jsonb `state` blob,
|
|
321
|
+
# where even a top-level scalar only ever comes out of `#>>` as
|
|
322
|
+
# text. A jsonb-extracted value (a value-object member reached
|
|
323
|
+
# through `nested_expression`/`jsonb_path` above) still comes out
|
|
324
|
+
# of `#>>` as text the exact same way PostgresEra's own does, and
|
|
325
|
+
# still needs the same `::numeric` cast to compare/sort
|
|
326
|
+
# numerically rather than lexicographically. `jsonb_extraction?`
|
|
327
|
+
# tells the two apart by inspecting the expression `query_expression`
|
|
328
|
+
# already built — never a second, hand-rolled field walk that
|
|
329
|
+
# could disagree with the one the SQL actually uses.
|
|
330
|
+
def comparable_expression(expression, value)
|
|
331
|
+
value.is_a?(Numeric) && jsonb_extraction?(expression) ? "(#{expression})::numeric" : expression
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def execute_query(sql, binds)
|
|
335
|
+
@db.exec_params(sql, binds).map do |row|
|
|
336
|
+
Runtime::Instance.new(aggregate: @aggregate, id: row["id"], state: decode(row))
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# ── the rest of the dialect ─────────────────────────────────────
|
|
341
|
+
|
|
342
|
+
def quote_ident(name) = PG::Connection.quote_ident(name.to_s)
|
|
343
|
+
def quoted_table = quote_ident(table)
|
|
344
|
+
def entry_table = "#{table}_entries"
|
|
345
|
+
def quoted_entry_table = quote_ident(entry_table)
|
|
346
|
+
|
|
347
|
+
def jsonb_extraction?(expression) = expression.include?("#>>")
|
|
348
|
+
|
|
349
|
+
def order_expression(field)
|
|
350
|
+
expression = query_expression(field)
|
|
351
|
+
jsonb_extraction?(expression) && numeric_field?(field) ? "(#{expression})::numeric" : expression
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# Postgres defaults to NULLS LAST on ASC — same override
|
|
355
|
+
# PostgresEra's own order_clause carries, so a declared query
|
|
356
|
+
# answers identically no matter which adapter serves it (the
|
|
357
|
+
# port's in-memory semantics, which Sqlite's own default happens
|
|
358
|
+
# to match, put null rows FIRST ascending and LAST descending).
|
|
359
|
+
def order_clause(order_by, policy)
|
|
360
|
+
direction = order_by.direction.to_s.downcase == "desc" ? "DESC" : "ASC"
|
|
361
|
+
nulls = case policy&.mode.to_s
|
|
362
|
+
when "first" then " NULLS FIRST"
|
|
363
|
+
when "last" then " NULLS LAST"
|
|
364
|
+
else direction == "DESC" ? " NULLS LAST" : " NULLS FIRST"
|
|
365
|
+
end
|
|
366
|
+
"#{order_expression(order_by.field)} #{direction}#{nulls}, id #{direction}"
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# Same walk PostgresEra's own numeric_field? uses — decides
|
|
370
|
+
# numericness at ANY depth from the declared shape itself, not a
|
|
371
|
+
# runtime value.
|
|
372
|
+
def numeric_field?(field)
|
|
373
|
+
name, *path = field.to_s.split(".")
|
|
374
|
+
QuerySpecification::FieldPath.numeric?(@aggregate.attribute(name), path) do |type|
|
|
375
|
+
@aggregate.value_object(type)
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# ARRAY[...] of individually-escaped literals — same escaping
|
|
380
|
+
# PostgresEra's own jsonb_path carries and the same reason: a
|
|
381
|
+
# hand-rolled '{a,b,c}' array literal has no escaping at all, and
|
|
382
|
+
# a segment is a field or value-object member name this method has
|
|
383
|
+
# no way to know is always schema-declared.
|
|
384
|
+
def jsonb_path(column, segments)
|
|
385
|
+
"#{quote_ident(column)} #>> ARRAY[#{segments.map { |segment| text_literal(segment) }.join(', ')}]::text[]"
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def text_literal(text) = "'#{text.to_s.gsub("'", "''")}'"
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
end
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
require_relative "../../../../runtime/era_tamper"
|
|
2
|
+
require_relative "../../../../runtime/registry"
|
|
3
|
+
require_relative "../../../../runtime/storage_shape"
|
|
4
|
+
|
|
5
|
+
module Hecks
|
|
6
|
+
module Adapters
|
|
7
|
+
class PostgresEra
|
|
8
|
+
class Lineage
|
|
9
|
+
module EraStore
|
|
10
|
+
# Every held text is verified against its raw-byte digest on the
|
|
11
|
+
# way out — an edited storage fact refuses rather than silently
|
|
12
|
+
# reporting "no drift". This is NOT the era name (which hashes
|
|
13
|
+
# the canonical projection, minted once, never recomputed): it
|
|
14
|
+
# is a plain integrity check over bytes, and a row from before
|
|
15
|
+
# the check existed is backfilled, not refused.
|
|
16
|
+
def eras
|
|
17
|
+
@db.exec_params(
|
|
18
|
+
"SELECT ordinal, hash, label, held_text, watermark, held_digest, held_projection::text " \
|
|
19
|
+
"FROM hecks_eras WHERE domain = $1 ORDER BY ordinal",
|
|
20
|
+
[@domain]
|
|
21
|
+
).map do |row|
|
|
22
|
+
verify_integrity!(row["ordinal"].to_i, row["held_text"], row["held_digest"], row["held_projection"])
|
|
23
|
+
{ ordinal: row["ordinal"].to_i, hash: row["hash"], label: row["label"],
|
|
24
|
+
held_text: row["held_text"], watermark: row["watermark"]&.to_i }
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# The unverified row — what bin/reattest_era shows an operator
|
|
29
|
+
# after the integrity check fires.
|
|
30
|
+
def raw_era(ordinal)
|
|
31
|
+
rows = @db.exec_params(
|
|
32
|
+
"SELECT held_text, held_digest, hash, held_projection::text FROM hecks_eras WHERE domain = $1 AND ordinal = $2",
|
|
33
|
+
[@domain, ordinal]
|
|
34
|
+
)
|
|
35
|
+
return nil if rows.ntuples.zero?
|
|
36
|
+
|
|
37
|
+
{ held_text: rows[0]["held_text"], held_digest: rows[0]["held_digest"], hash: rows[0]["hash"],
|
|
38
|
+
held_projection: rows[0]["held_projection"] }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# The recovery path: tamper-EVIDENCE (against accident and
|
|
42
|
+
# drift, not adversaries) gets resolved by a human acknowledging
|
|
43
|
+
# the text as it now stands — recorded in an append-only
|
|
44
|
+
# attestation table, never by a bare psql UPDATE.
|
|
45
|
+
def reattest!(ordinal)
|
|
46
|
+
era = raw_era(ordinal)
|
|
47
|
+
raise Runtime::WiringError, "#{@domain} holds no era #{ordinal} to re-attest" unless era
|
|
48
|
+
|
|
49
|
+
fresh = Digest::SHA256.hexdigest(era[:held_text])
|
|
50
|
+
@db.exec(<<~SQL)
|
|
51
|
+
CREATE TABLE IF NOT EXISTS hecks_attestations (
|
|
52
|
+
domain text NOT NULL,
|
|
53
|
+
ordinal int NOT NULL,
|
|
54
|
+
old_digest text,
|
|
55
|
+
new_digest text NOT NULL,
|
|
56
|
+
attested_at timestamptz NOT NULL DEFAULT now()
|
|
57
|
+
)
|
|
58
|
+
SQL
|
|
59
|
+
@db.exec_params(
|
|
60
|
+
"INSERT INTO hecks_attestations (domain, ordinal, old_digest, new_digest) VALUES ($1, $2, $3, $4)",
|
|
61
|
+
[@domain, ordinal, era[:held_digest], fresh]
|
|
62
|
+
)
|
|
63
|
+
@db.exec_params(
|
|
64
|
+
"UPDATE hecks_eras SET held_digest = $3 WHERE domain = $1 AND ordinal = $2",
|
|
65
|
+
[@domain, ordinal, fresh]
|
|
66
|
+
)
|
|
67
|
+
archive_text!(ordinal, era[:held_text])
|
|
68
|
+
fresh
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def verify_integrity!(ordinal, text, stored_digest, stored_projection_json)
|
|
72
|
+
digest = Digest::SHA256.hexdigest(text)
|
|
73
|
+
if stored_digest.nil?
|
|
74
|
+
@db.exec_params(
|
|
75
|
+
"UPDATE hecks_eras SET held_digest = $3 WHERE domain = $1 AND ordinal = $2 AND held_digest IS NULL",
|
|
76
|
+
[@domain, ordinal, digest]
|
|
77
|
+
)
|
|
78
|
+
backfill_frozen_facts!(ordinal, text, stored_projection_json)
|
|
79
|
+
return
|
|
80
|
+
end
|
|
81
|
+
if stored_digest == digest
|
|
82
|
+
backfill_frozen_facts!(ordinal, text, stored_projection_json)
|
|
83
|
+
return
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
raise Runtime::WiringError, Runtime::EraTamper.refusal(domain: @domain, ordinal: ordinal)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# A verified-authentic text backfills what older rows lack: its
|
|
90
|
+
# projection and its archive copy. Never run on a text that
|
|
91
|
+
# failed verification — that would bless the edit.
|
|
92
|
+
def backfill_frozen_facts!(ordinal, text, stored_projection_json)
|
|
93
|
+
archive_text!(ordinal, text)
|
|
94
|
+
return if stored_projection_json
|
|
95
|
+
|
|
96
|
+
projection = Runtime::EraTamper.project(text)
|
|
97
|
+
return unless projection
|
|
98
|
+
|
|
99
|
+
@db.exec_params(
|
|
100
|
+
"UPDATE hecks_eras SET held_projection = $3::text::jsonb " \
|
|
101
|
+
"WHERE domain = $1 AND ordinal = $2 AND held_projection IS NULL",
|
|
102
|
+
[@domain, ordinal, JSON.generate(projection)]
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def current_era
|
|
107
|
+
held = eras
|
|
108
|
+
held.empty? ? 1 : held.last[:ordinal]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def hold_first!(text, projection: nil)
|
|
112
|
+
@db.exec_params(
|
|
113
|
+
"INSERT INTO hecks_eras (domain, ordinal, held_text, held_digest, held_projection) " \
|
|
114
|
+
"VALUES ($1, 1, $2, $3, $4) ON CONFLICT DO NOTHING",
|
|
115
|
+
[@domain, text, Digest::SHA256.hexdigest(text), projection && JSON.generate(projection)]
|
|
116
|
+
)
|
|
117
|
+
archive_text!(1, text)
|
|
118
|
+
# Era 1 is the current era from its first moment — established
|
|
119
|
+
# here, not left to whichever role happens to boot first.
|
|
120
|
+
advance_era!(1)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def archive_text!(ordinal, text)
|
|
124
|
+
@db.exec_params(
|
|
125
|
+
"INSERT INTO hecks_era_texts (domain, ordinal, digest, held_text) VALUES ($1, $2, $3, $4) " \
|
|
126
|
+
"ON CONFLICT DO NOTHING",
|
|
127
|
+
[@domain, ordinal, Digest::SHA256.hexdigest(text), text]
|
|
128
|
+
)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# A Layer-3 approval, recorded IN the database it was reviewed
|
|
132
|
+
# against — bound to the edge's parsed content and the journal's
|
|
133
|
+
# high-water ordinal at review time. The latest row for a shape
|
|
134
|
+
# pair wins (re-approval supersedes).
|
|
135
|
+
def record_approval!(from:, to:, edge_digest:)
|
|
136
|
+
@db.exec_params(
|
|
137
|
+
"INSERT INTO hecks_approvals (domain, from_label, to_label, edge_digest, reviewed_ordinal) " \
|
|
138
|
+
"VALUES ($1, $2, $3, $4, $5)",
|
|
139
|
+
[@domain, from, to, edge_digest, last_ordinal]
|
|
140
|
+
)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def approval_for(from:, to:)
|
|
144
|
+
rows = @db.exec_params(
|
|
145
|
+
"SELECT edge_digest, reviewed_ordinal FROM hecks_approvals " \
|
|
146
|
+
"WHERE domain = $1 AND from_label = $2 AND to_label = $3 ORDER BY approved_at DESC, reviewed_ordinal DESC LIMIT 1",
|
|
147
|
+
[@domain, from, to]
|
|
148
|
+
)
|
|
149
|
+
return nil if rows.ntuples.zero?
|
|
150
|
+
|
|
151
|
+
{ edge_digest: rows[0]["edge_digest"], reviewed_ordinal: rows[0]["reviewed_ordinal"].to_i }
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Era identity is minted ONCE and stored; nothing ever recomputes
|
|
155
|
+
# a stored name to verify it.
|
|
156
|
+
def mint_name!(ordinal, hash, label)
|
|
157
|
+
@db.exec_params(
|
|
158
|
+
"UPDATE hecks_eras SET hash = $3, label = $4, canon_form = $5 " \
|
|
159
|
+
"WHERE domain = $1 AND ordinal = $2 AND hash IS NULL",
|
|
160
|
+
[@domain, ordinal, hash, label, Runtime::StorageShape::FORM_VERSION]
|
|
161
|
+
)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def last_ordinal
|
|
165
|
+
@db.exec("SELECT COALESCE(max(ordinal), 0) AS o FROM #{quoted_journal}")[0]["o"].to_i
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|