hecks 0.2.0 → 1.0.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 +427 -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 +101 -0
- data/lib/hecks/adapters/driven/heki/journal.rb +56 -0
- data/lib/hecks/adapters/driven/heki/saga_store.rb +99 -0
- data/lib/hecks/adapters/driven/heki/snapshot.rb +65 -0
- data/lib/hecks/adapters/driven/heki.adapter +4 -0
- data/lib/hecks/adapters/driven/heki.rb +179 -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 +145 -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 +88 -0
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +207 -0
- data/lib/hecks/adapters/driven/postgres.adapter +5 -0
- data/lib/hecks/adapters/driven/postgres.rb +438 -0
- data/lib/hecks/adapters/driven/postgres_era.adapter +17 -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 +80 -0
- data/lib/hecks/adapters/driven/sqlite/projection.rb +172 -0
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +192 -0
- data/lib/hecks/adapters/driven/sqlite.adapter +9 -0
- data/lib/hecks/adapters/driven/sqlite.rb +306 -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 +54 -0
- data/lib/hecks/adapters.rb +6 -0
- data/lib/hecks/behaviors/dsl.rb +125 -0
- data/lib/hecks/behaviors/expectations.rb +340 -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 +438 -0
- data/lib/hecks/bluebook/assembly/marks.rb +228 -0
- data/lib/hecks/bluebook/assembly/specializer.rb +70 -0
- data/lib/hecks/bluebook/assembly.rb +91 -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 +68 -0
- data/lib/hecks/bluebook/behaviour/policy.rb +52 -0
- data/lib/hecks/bluebook/behaviour/process_manager.rb +51 -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 +78 -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 +1018 -0
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +348 -0
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +71 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +1087 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +714 -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 +430 -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 +135 -0
- data/lib/hecks/bluebook/dsl/port_builder.rb +39 -0
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +142 -0
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +306 -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 +173 -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 +228 -0
- data/lib/hecks/bluebook/dsl/world_builder.rb +117 -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 +123 -0
- data/lib/hecks/bluebook/expression/evaluator.rb +304 -0
- data/lib/hecks/bluebook/expression/projection.json +218 -0
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +233 -0
- data/lib/hecks/bluebook/expression/resolver.rb +781 -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 +621 -0
- data/lib/hecks/bluebook/meta_validator/plan.rb +332 -0
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +51 -0
- data/lib/hecks/bluebook/meta_validator/readings.rb +360 -0
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +351 -0
- data/lib/hecks/bluebook/meta_validator/shapes.rb +266 -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 +542 -0
- data/lib/hecks/bluebook/model_check.rb +445 -0
- data/lib/hecks/bluebook/pattern_subset.rb +184 -0
- data/lib/hecks/bluebook/policy.rb +41 -0
- data/lib/hecks/bluebook/process_manager.rb +128 -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 +66 -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 +410 -0
- data/lib/hecks/embryonaut_bluebook.rb +75 -0
- data/lib/hecks/facade/cli_door.rb +119 -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 +166 -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 +341 -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 +177 -0
- data/lib/hecks/forms/field_shape.rb +232 -0
- data/lib/hecks/forms/html.rb +84 -0
- data/lib/hecks/forms/index_renderer.rb +35 -0
- data/lib/hecks/forms/page.rb +157 -0
- data/lib/hecks/forms/params.rb +161 -0
- data/lib/hecks/forms/port_argument.rb +46 -0
- data/lib/hecks/forms/query_form_renderer.rb +114 -0
- data/lib/hecks/forms/record_renderer.rb +119 -0
- data/lib/hecks/forms/record_table.rb +68 -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 +145 -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/bounded_exhaustive_expressions.rb +527 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +86 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +286 -0
- data/lib/hecks/fuzzing/properties.rb +1192 -0
- data/lib/hecks/fuzzing/replay.rb +668 -0
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +93 -0
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +80 -0
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +96 -0
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +250 -0
- data/lib/hecks/fuzzing/sequence_generator.rb +131 -0
- data/lib/hecks/fuzzing/value_generator.rb +200 -0
- data/lib/hecks/fuzzing.rb +12 -0
- data/lib/hecks/grammar/evolve.rb +324 -0
- data/lib/hecks/grammar/expression.bluebook +420 -0
- data/lib/hecks/grammar/expression_operators.json +1648 -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 +471 -0
- data/lib/hecks/language/bluebook/entity.bluebook +392 -0
- data/lib/hecks/language/bluebook/policy.bluebook +189 -0
- data/lib/hecks/language/bluebook/process_manager.bluebook +381 -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 +448 -0
- data/lib/hecks/language/bluebook/vocabulary.bluebook +379 -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 +333 -0
- data/lib/hecks/language/port.bluebook +120 -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 +174 -0
- data/lib/hecks/ports/access_control.port +9 -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 +8 -0
- data/lib/hecks/ports/agent.rb +167 -0
- data/lib/hecks/ports/authentication.port +6 -0
- data/lib/hecks/ports/authentication.rb +50 -0
- data/lib/hecks/ports/authorization.port +7 -0
- data/lib/hecks/ports/authorization.rb +62 -0
- data/lib/hecks/ports/clock.port +5 -0
- data/lib/hecks/ports/clock.rb +62 -0
- data/lib/hecks/ports/extraction.port +5 -0
- data/lib/hecks/ports/extraction.rb +37 -0
- data/lib/hecks/ports/identity_assignment.port +5 -0
- data/lib/hecks/ports/identity_assignment.rb +45 -0
- data/lib/hecks/ports/identity_generation.port +5 -0
- data/lib/hecks/ports/identity_generation.rb +49 -0
- data/lib/hecks/ports/identity_resolution.port +5 -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 +138 -0
- data/lib/hecks/ports/persistence/binding_policy.rb +56 -0
- data/lib/hecks/ports/persistence/execution.rb +23 -0
- data/lib/hecks/ports/persistence/null_saga_store.rb +25 -0
- data/lib/hecks/ports/persistence/plugin.rb +54 -0
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +169 -0
- data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +128 -0
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +161 -0
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +61 -0
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +304 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +171 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +190 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +478 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +166 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +314 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +168 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +170 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +134 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +137 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +89 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +85 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +43 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +132 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +74 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +758 -0
- data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +120 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +31 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +46 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +102 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +46 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +70 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +72 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +120 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +183 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +41 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +28 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +30 -0
- data/lib/hecks/ports/persistence/plugins/era/translation.rb +11 -0
- data/lib/hecks/ports/persistence/plugins/era.rb +48 -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 +96 -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 +642 -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 +44 -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 +446 -0
- data/lib/hecks/projector/docs_projector.rb +321 -0
- data/lib/hecks/projector/exporter.rb +158 -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 +411 -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 +92 -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 +107 -0
- data/lib/hecks/query_specification/hop_path.rb +132 -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/aggregate_lock.rb +45 -0
- data/lib/hecks/runtime/boot_gates.rb +41 -0
- data/lib/hecks/runtime/caller.rb +66 -0
- data/lib/hecks/runtime/capability_graph.rb +44 -0
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +135 -0
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +260 -0
- data/lib/hecks/runtime/command_interpreter.rb +497 -0
- data/lib/hecks/runtime/command_rules/admissibility.rb +357 -0
- data/lib/hecks/runtime/command_rules/arithmetic.rb +260 -0
- data/lib/hecks/runtime/command_rules/authorization.rb +65 -0
- data/lib/hecks/runtime/command_rules/emission.rb +34 -0
- data/lib/hecks/runtime/command_rules/references.rb +189 -0
- data/lib/hecks/runtime/command_rules.rb +28 -0
- data/lib/hecks/runtime/dependency_planning.rb +245 -0
- data/lib/hecks/runtime/dispatcher.rb +301 -0
- data/lib/hecks/runtime/entity_element.rb +253 -0
- data/lib/hecks/runtime/entity_interpreter.rb +299 -0
- data/lib/hecks/runtime/errors.rb +122 -0
- data/lib/hecks/runtime/event.rb +51 -0
- data/lib/hecks/runtime/identity.rb +156 -0
- data/lib/hecks/runtime/instance.rb +175 -0
- data/lib/hecks/runtime/interpreting.rb +90 -0
- data/lib/hecks/runtime/loader.rb +184 -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 +286 -0
- data/lib/hecks/runtime/reaction_invocation.rb +253 -0
- data/lib/hecks/runtime/read_model_interpreter.rb +341 -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 +126 -0
- data/lib/hecks/runtime/registry/saga_persistence.rb +142 -0
- data/lib/hecks/runtime/registry/verification.rb +246 -0
- data/lib/hecks/runtime/registry.rb +279 -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 +503 -0
- data/lib/hecks/runtime/saga_pending_dispatch.rb +45 -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 +130 -0
- data/lib/hecks/runtime/value/coercion.rb +565 -0
- data/lib/hecks/runtime/value/invariant_violation.rb +5 -0
- data/lib/hecks/runtime/value.rb +125 -0
- data/lib/hecks/runtime.rb +107 -0
- data/lib/hecks/storehouse.rb +632 -0
- data/lib/hecks/version.rb +16 -0
- data/lib/hecks/vocabulary.rb +218 -0
- data/lib/hecks.rb +123 -6
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +90 -0
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +128 -0
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +160 -0
- metadata +412 -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,304 @@
|
|
|
1
|
+
require_relative "../../../../naming"
|
|
2
|
+
require_relative "../../append_only"
|
|
3
|
+
require_relative "../../../../runtime/registry"
|
|
4
|
+
|
|
5
|
+
module Hecks
|
|
6
|
+
module Ports
|
|
7
|
+
module Persistence
|
|
8
|
+
# Translates a journal entry written under an old shape into the one
|
|
9
|
+
# the current bluebook declares — a renamed attribute, a renamed
|
|
10
|
+
# aggregate, or a field crossing a value-object boundary. Applied
|
|
11
|
+
# wherever entries are read, so replay derives the current head from
|
|
12
|
+
# history that was never rewritten — the port owns the meaning,
|
|
13
|
+
# adapters stay oblivious.
|
|
14
|
+
class Lineage
|
|
15
|
+
# `ancestor_name` is the declared name a rename came from (matches
|
|
16
|
+
# the held bluebook's own aggregate names); `ancestor_storage_name`
|
|
17
|
+
# is its derived, snake_case file/table name. Both are nil unless
|
|
18
|
+
# the aggregate itself was renamed.
|
|
19
|
+
attr_reader :ancestor_name, :ancestor_storage_name
|
|
20
|
+
|
|
21
|
+
def self.for(registry, domain, aggregate)
|
|
22
|
+
translation = registry.translations.find do |candidate|
|
|
23
|
+
candidate.domain == domain.to_s && candidate.for_aggregate(aggregate.name)
|
|
24
|
+
end
|
|
25
|
+
return nil unless translation
|
|
26
|
+
|
|
27
|
+
from_declared(translation.for_aggregate(aggregate.name), aggregate.name)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# One SPECIFIC edge's rules for one aggregate — what the mint
|
|
31
|
+
# path uses, where `for` would happily answer with whichever
|
|
32
|
+
# edge in the registry mentioned the aggregate first.
|
|
33
|
+
def self.from_declared(declared, aggregate_name)
|
|
34
|
+
return nil unless declared
|
|
35
|
+
|
|
36
|
+
renamed_aggregate = declared.was && declared.was != aggregate_name.to_s
|
|
37
|
+
ancestor_name = renamed_aggregate ? declared.was : nil
|
|
38
|
+
ancestor_storage_name = renamed_aggregate ? Naming.snake(declared.was) : nil
|
|
39
|
+
|
|
40
|
+
new(declared.renames, declared.moves, declared.converts, declared.drops,
|
|
41
|
+
retypes: declared.retypes, computes: declared.computes, rekeys: declared.rekeys,
|
|
42
|
+
backfills: declared.backfills,
|
|
43
|
+
ancestor_name: ancestor_name, ancestor_storage_name: ancestor_storage_name)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def initialize(renames, moves = [], converts = [], drops = [], retypes: [], computes: [], rekeys: [],
|
|
47
|
+
backfills: [], ancestor_name: nil, ancestor_storage_name: nil)
|
|
48
|
+
@renames = renames
|
|
49
|
+
@moves = moves
|
|
50
|
+
@converts = converts
|
|
51
|
+
@drops = drops
|
|
52
|
+
@retypes = retypes
|
|
53
|
+
@computes = computes
|
|
54
|
+
@rekeys = rekeys
|
|
55
|
+
@backfills = backfills
|
|
56
|
+
@ancestor_name = ancestor_name
|
|
57
|
+
@ancestor_storage_name = ancestor_storage_name
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Whether any rule in this edge is a compute — the one rule kind
|
|
61
|
+
# with no in-process implementation at all. An aggregate carrying
|
|
62
|
+
# one refuses to boot anywhere but Postgres, per-rule and by name,
|
|
63
|
+
# before the general drift machinery says anything vaguer.
|
|
64
|
+
def computes? = !@computes.empty?
|
|
65
|
+
|
|
66
|
+
# THE SINGLE SOURCE OF TRUTH for "does this edge rekey this
|
|
67
|
+
# aggregate" — every consumer (coverage_check.rb's identity gate,
|
|
68
|
+
# minter.rb's approval gate, layer_two.rb's audit, head_compiler.rb's
|
|
69
|
+
# SQL compilation) asks THIS, never re-derives it from `declared`
|
|
70
|
+
# independently. One accessor to change if what a rekey rule means
|
|
71
|
+
# ever needs to change, not four call sites in four files.
|
|
72
|
+
def rekey? = !@rekeys.empty?
|
|
73
|
+
|
|
74
|
+
# The rekey's own SQL — first-and-only rule, same one-per-aggregate
|
|
75
|
+
# assumption `compute` makes about its own list where it matters
|
|
76
|
+
# (an edge with more than one is a DSL-level decision, not
|
|
77
|
+
# something this reader arbitrates).
|
|
78
|
+
def rekey_sql = @rekeys.first&.sql
|
|
79
|
+
|
|
80
|
+
# The reference semantics for the five PORTABLE rule kinds —
|
|
81
|
+
# rename, move, convert, drop, and the aggregate-level `was:`.
|
|
82
|
+
# `retype` moves nothing (stored state never carries a type name)
|
|
83
|
+
# and `compute` is deliberately not applied here: its SQL is its
|
|
84
|
+
# only implementation, so this transform neither imitates nor
|
|
85
|
+
# checks it — the source field passes through untouched, and the
|
|
86
|
+
# audit verifies compute output against the matview alone.
|
|
87
|
+
def translate(entry)
|
|
88
|
+
return entry unless entry.save? && entry.state
|
|
89
|
+
|
|
90
|
+
# Deep, not shallow: a move or convert reaches INTO a nested
|
|
91
|
+
# value-object hash, and a shallow dup would quietly mutate
|
|
92
|
+
# the caller's copy of the original entry.
|
|
93
|
+
state = deep_dup(entry.state)
|
|
94
|
+
apply_renames(state, @renames)
|
|
95
|
+
@moves.each { |move| apply_move(state, move) }
|
|
96
|
+
@converts.each { |convert| apply_convert(state, convert) }
|
|
97
|
+
@drops.each { |name| apply_drop(state, name) }
|
|
98
|
+
# LAST, and only where nothing already answered — a backfill
|
|
99
|
+
# fills the gap a rename/move/convert left untouched, never
|
|
100
|
+
# overwrites a value that already made it across.
|
|
101
|
+
@backfills.each { |backfill| state[backfill.name] = backfill.default unless state.key?(backfill.name) }
|
|
102
|
+
Entry.new(operation: entry.operation, id: entry.id, state: state, mirrors: entry.mirrors)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Whether this translation names `path` as an old key it accounts
|
|
106
|
+
# for — the rename, move, or convert it came from, or an explicit
|
|
107
|
+
# drop. `path` is a bare name ("cost") or a dotted value-object
|
|
108
|
+
# member ("price.currency"); a rule covering the WHOLE top-level
|
|
109
|
+
# attribute (a rename, a top-level move/convert/drop) also covers
|
|
110
|
+
# anything nested under it, since the whole value travels or goes
|
|
111
|
+
# away together. Used to catch a field — or a value object's own
|
|
112
|
+
# member — that vanished (or silently changed type) without
|
|
113
|
+
# anything explaining it, even when some OTHER field is covered.
|
|
114
|
+
#
|
|
115
|
+
# `backfills` matches on the WHOLE name only, never a dotted
|
|
116
|
+
# prefix — a backfill names a top-level attribute that is new
|
|
117
|
+
# outright (nothing to be a prefix of on the held side), unlike
|
|
118
|
+
# every rule above it, which explains a path that existed and
|
|
119
|
+
# moved, converted, or vanished.
|
|
120
|
+
def explains?(path)
|
|
121
|
+
path = path.to_s
|
|
122
|
+
top = path.split(".").first
|
|
123
|
+
|
|
124
|
+
@renames.key?(top.to_sym) ||
|
|
125
|
+
@moves.any? { |move| move.from == path || move.from.split(".").first == top } ||
|
|
126
|
+
@converts.any? { |convert| convert.from == path || convert.from.split(".").first == top } ||
|
|
127
|
+
@drops.any? { |drop| drop.to_s == path || drop.to_s.split(".").first == top } ||
|
|
128
|
+
@computes.any? { |compute| compute.from == path || compute.from.split(".").first == top } ||
|
|
129
|
+
@backfills.any? { |backfill| backfill.name.to_s == top }
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# THE DESTINATION-SIDE TWIN of `explains?` above, which only ever
|
|
133
|
+
# asks about a rule's SOURCE. `unsafe_additions` asks a different
|
|
134
|
+
# question — not "was this vanished path accounted for" but "does
|
|
135
|
+
# an existing record end up with a value here" — and a move or
|
|
136
|
+
# convert whose `to:` lands a old field inside a BRAND-NEW
|
|
137
|
+
# top-level attribute (`weight` becoming `contents.weight` when
|
|
138
|
+
# `Contents` did not exist before) fills that attribute for an
|
|
139
|
+
# existing record exactly as a `backfill` would, even though
|
|
140
|
+
# nothing named `contents` explains any vanished path. `compute`
|
|
141
|
+
# counts on the same terms `explains?` already grants it
|
|
142
|
+
# elsewhere in this file — Postgres-only and audited, not
|
|
143
|
+
# actually applied by THIS method, the same gap the vanish side
|
|
144
|
+
# already lives with.
|
|
145
|
+
#
|
|
146
|
+
# `@renames.value?` belongs here too, and used to be missing: a
|
|
147
|
+
# bare `rename :cost, to: :amount` is the plainest possible
|
|
148
|
+
# covering rule there is (`translate` above applies it
|
|
149
|
+
# unconditionally, no lookup table, no per-record ambiguity —
|
|
150
|
+
# simpler than a move or convert, which both got their `fills?`
|
|
151
|
+
# entry from the start), and its absence meant `unsafe_additions`
|
|
152
|
+
# reported the new name as an unexplained required addition on
|
|
153
|
+
# EVERY rename-only edge, the single most common translation
|
|
154
|
+
# shape there is. `explains?` already checked the source side
|
|
155
|
+
# (`@renames.key?`); `fills?` is the symmetric destination-side
|
|
156
|
+
# check that was never added alongside it.
|
|
157
|
+
def fills?(path)
|
|
158
|
+
path = path.to_s
|
|
159
|
+
|
|
160
|
+
@renames.value?(path.to_sym) ||
|
|
161
|
+
@moves.any? { |move| move.to.split(".").first == path } ||
|
|
162
|
+
@converts.any? { |convert| convert.to.split(".").first == path } ||
|
|
163
|
+
@computes.any? { |compute| compute.to.split(".").first == path } ||
|
|
164
|
+
@backfills.any? { |backfill| backfill.name.to_s == path }
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Whether a declared retype says the pair of TYPE names means the
|
|
168
|
+
# same shape — a value object or entity whose own name changed
|
|
169
|
+
# with its members intact. Nothing in the stored data carries the
|
|
170
|
+
# type name, so this never moves a value; it only satisfies the
|
|
171
|
+
# era diff's literal type-name comparison.
|
|
172
|
+
def retype?(held_type, current_type)
|
|
173
|
+
@retypes.any? { |retype| retype.from == held_type.to_s && retype.to == current_type.to_s }
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
private
|
|
177
|
+
|
|
178
|
+
def deep_dup(node)
|
|
179
|
+
case node
|
|
180
|
+
when Hash then node.transform_values { |value| deep_dup(value) }
|
|
181
|
+
when Array then node.map { |item| deep_dup(item) }
|
|
182
|
+
else node
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# M27 (docs/audits/2026-08-10-main-bug-audit.md,
|
|
187
|
+
# docs/audits/2026-08-11-bug-triage.md) — SIMULTANEOUS, not
|
|
188
|
+
# sequential: `state[new] = state.delete(old)` per rename, run
|
|
189
|
+
# one rule at a time against the SAME hash it was reading from,
|
|
190
|
+
# loses data the instant one rule's destination is another
|
|
191
|
+
# rule's source. A swap (`rename :a, to: :b` alongside
|
|
192
|
+
# `rename :b, to: :a`) on `{a: 1, b: 2}` used to produce
|
|
193
|
+
# `{a: 1}` — the first rule wrote `b: 1` over the real `b: 2`
|
|
194
|
+
# before the second rule ever got a chance to read it, and the
|
|
195
|
+
# value the whole edge was supposed to preserve (2, moved to
|
|
196
|
+
# `:a`) was gone. The standard fix: snapshot every rule's OLD
|
|
197
|
+
# key and value from `state` FIRST, then remove every old key
|
|
198
|
+
# and only THEN write every new key — a rename never reads a
|
|
199
|
+
# key this same pass has already written to, so a swap or a
|
|
200
|
+
# longer chain applies as one permutation, not a sequence of
|
|
201
|
+
# edits each stepping on the last.
|
|
202
|
+
def apply_renames(state, renames)
|
|
203
|
+
snapshot = renames.filter_map { |old_name, new_name| [old_name, new_name, state[old_name]] if state.key?(old_name) }
|
|
204
|
+
snapshot.each { |old_name, _new_name, _value| state.delete(old_name) }
|
|
205
|
+
# rubocop:disable Style/CombinableLoops -- NOT combinable: a swap
|
|
206
|
+
# (:a<->:b) needs every delete done before any write, or the first
|
|
207
|
+
# rename's write becomes the second rename's delete target — see
|
|
208
|
+
# this method's own comment above.
|
|
209
|
+
snapshot.each { |_old_name, new_name, value| state[new_name] = value }
|
|
210
|
+
# rubocop:enable Style/CombinableLoops
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def apply_drop(state, name)
|
|
214
|
+
name = name.to_s
|
|
215
|
+
top, member = name.split(".", 2)
|
|
216
|
+
top = top.to_sym
|
|
217
|
+
|
|
218
|
+
if member
|
|
219
|
+
nested = state[top]
|
|
220
|
+
if nested.is_a?(Hash)
|
|
221
|
+
nested.delete(member)
|
|
222
|
+
state.delete(top) if nested.empty?
|
|
223
|
+
end
|
|
224
|
+
else
|
|
225
|
+
state.delete(top)
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# A dotted path's first segment is a top-level (symbol) key; a
|
|
230
|
+
# second segment reaches into a value-object member, stored with a
|
|
231
|
+
# string key exactly as the adapter's own journal reader loaded it.
|
|
232
|
+
def apply_move(state, move)
|
|
233
|
+
old_top, old_member = move.from.split(".", 2)
|
|
234
|
+
new_top, new_member = move.to.split(".", 2)
|
|
235
|
+
old_top = old_top.to_sym
|
|
236
|
+
|
|
237
|
+
value, present = extract(state, old_top, old_member)
|
|
238
|
+
return unless present
|
|
239
|
+
|
|
240
|
+
insert(state, new_top.to_sym, new_member, value, rule: "move #{move.from} to: #{move.to}")
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# A convert is a move whose value has nothing in common with its
|
|
244
|
+
# replacement — the same path machinery, plus a lookup. A value
|
|
245
|
+
# with no entry in the table refuses loudly rather than carry an
|
|
246
|
+
# unrecognized value silently into the new era.
|
|
247
|
+
def apply_convert(state, convert)
|
|
248
|
+
old_top, old_member = convert.from.split(".", 2)
|
|
249
|
+
new_top, new_member = convert.to.split(".", 2)
|
|
250
|
+
old_top = old_top.to_sym
|
|
251
|
+
|
|
252
|
+
raw, present = extract(state, old_top, old_member)
|
|
253
|
+
return unless present
|
|
254
|
+
|
|
255
|
+
unless convert.values.key?(raw)
|
|
256
|
+
raise Runtime::WiringError,
|
|
257
|
+
"cannot translate #{convert.from}: #{raw.inspect} has no mapping in its " \
|
|
258
|
+
"convert's values: table. Add #{raw.inspect} => ... to cover it."
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
insert(state, new_top.to_sym, new_member, convert.values[raw], rule: "convert #{convert.from} to: #{convert.to}")
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def extract(state, top, member)
|
|
265
|
+
return [nil, false] unless state.key?(top)
|
|
266
|
+
return [state.delete(top), true] unless member
|
|
267
|
+
|
|
268
|
+
nested = state[top]
|
|
269
|
+
return [nil, false] unless nested.is_a?(Hash) && nested.key?(member)
|
|
270
|
+
|
|
271
|
+
value = nested.delete(member)
|
|
272
|
+
state.delete(top) if nested.empty?
|
|
273
|
+
[value, true]
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# ADVERSARIAL FINDING, not a hypothetical: a destination whose
|
|
277
|
+
# top segment ALREADY holds a value — most commonly a reference,
|
|
278
|
+
# stored as a bare scalar id — used to be silently clobbered with
|
|
279
|
+
# an empty hash the moment a dotted destination needed to nest
|
|
280
|
+
# under it (`state[top] ||= {}` only guards nil/false, so a
|
|
281
|
+
# truthy non-Hash sailed straight through to `state[top][member] =`,
|
|
282
|
+
# i.e. `"team-1"["detail"] =`, which is String#[]= and raised an
|
|
283
|
+
# unrelated-looking IndexError). Whether it crashed or silently
|
|
284
|
+
# replaced the value, this is a `drop` that never declared
|
|
285
|
+
# itself — the one thing this language exists to make explicit
|
|
286
|
+
# (see apply_convert's own refusal above, the same shape). Refuse
|
|
287
|
+
# by name instead, on both sides: the SQL half (hecks_tr_insert)
|
|
288
|
+
# raises the identical wording.
|
|
289
|
+
def insert(state, top, member, value, rule:)
|
|
290
|
+
return state[top] = value unless member
|
|
291
|
+
|
|
292
|
+
if state.key?(top) && !state[top].is_a?(Hash)
|
|
293
|
+
raise Runtime::WiringError,
|
|
294
|
+
"cannot #{rule}: #{top} already holds #{state[top].inspect}, not a value this can nest " \
|
|
295
|
+
"under — moving into it would discard that value silently. Rename or drop #{top} first."
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
state[top] ||= {}
|
|
299
|
+
state[top][member] = value
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
end
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
require_relative "../../era_tamper"
|
|
2
|
+
require_relative "../../../../../../runtime/registry"
|
|
3
|
+
require_relative "../../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
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
require "digest"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Adapters
|
|
5
|
+
class PostgresEra
|
|
6
|
+
class Lineage
|
|
7
|
+
# THE READ-SIDE OF THE ERA WORKAROUND. Once a domain mints a
|
|
8
|
+
# second era, `head_view` is a `DISTINCT ON`/`UNION ALL`
|
|
9
|
+
# reduction — and a `where` clause on anything but `id` (its own
|
|
10
|
+
# partition key) cannot be pushed through that reduction, so every
|
|
11
|
+
# declared query pays the cost of reducing the WHOLE aggregate
|
|
12
|
+
# before it can filter anything (see docs/implemented/postgres-era-adapter-
|
|
13
|
+
# split-plan.md's own trigger section — this is a real
|
|
14
|
+
# SQL-semantics wall, not a missing index; adding one to
|
|
15
|
+
# `head_view` itself changes nothing, because the planner cannot
|
|
16
|
+
# prove a predicate on a non-partition column commutes with
|
|
17
|
+
# `DISTINCT ON` without risking a stale row winning).
|
|
18
|
+
#
|
|
19
|
+
# THE WORKAROUND: one narrow table per declared `where`-field
|
|
20
|
+
# (never `order_by`-only fields — sorting the reduced output was
|
|
21
|
+
# never blocked by the reduction; only filtering was), holding
|
|
22
|
+
# exactly (id, ordinal, value) — the CURRENT extracted value for
|
|
23
|
+
# every live id, maintained transactionally by `append` alongside
|
|
24
|
+
# the head snapshot it already upserts (same ordinal-guard idiom:
|
|
25
|
+
# `WHERE ordinal < EXCLUDED.ordinal`). A query whose sole `where`
|
|
26
|
+
# is on a cached field never runs the reduction at all: `SELECT id
|
|
27
|
+
# FROM <field>_cache WHERE value <op> $1`, then `SELECT id, state
|
|
28
|
+
# FROM head_view WHERE id = ANY($ids)` — safe THROUGH the
|
|
29
|
+
# reduction because `id` is its own partition key, unlike the
|
|
30
|
+
# original field.
|
|
31
|
+
#
|
|
32
|
+
# SCOPED TO NON-LIST FIELDS ONLY, same reasoning Track A (plain
|
|
33
|
+
# Postgres) and Track B (Sqlite/D1) both landed on independently
|
|
34
|
+
# for their own automatic indexing: this codebase's `contains`
|
|
35
|
+
# compiles to element-membership (`jsonb_array_elements`/
|
|
36
|
+
# `json_each` + `EXISTS`), which a (id, ordinal, value) cache table
|
|
37
|
+
# keyed on ONE scalar value per id has no way to represent — a
|
|
38
|
+
# list field would need one row per (id, element), a different
|
|
39
|
+
# shape this plan does not build. `PostgresEra#eligible_for_cache?`
|
|
40
|
+
# is where that boundary lives.
|
|
41
|
+
module FieldCache
|
|
42
|
+
# Deterministic and collision-free regardless of field-path
|
|
43
|
+
# length or storage-name length — Postgres identifiers cap at 63
|
|
44
|
+
# bytes, and a value-object member path (`compliance.review.
|
|
45
|
+
# deadline_at`) combined with a long storage name could exceed
|
|
46
|
+
# that if built any less defensively. Not meant to be
|
|
47
|
+
# human-readable; `hecks_backfill_progress`/catalog lookups are
|
|
48
|
+
# always driven by this same computed name, never typed by hand.
|
|
49
|
+
def field_cache(storage_name, era, field)
|
|
50
|
+
"hecks_fc_#{Digest::SHA256.hexdigest("#{storage_name}\0#{era}\0#{field}")[0, 20]}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Self-healing, same idiom as `ensure_head_snapshot!`: cheap,
|
|
54
|
+
# unconditional, safe to call on every boot. CREATION is a
|
|
55
|
+
# short-held lock (table doesn't exist yet — nothing to block);
|
|
56
|
+
# BACKFILL runs outside it, chunk by chunk, each chunk its own
|
|
57
|
+
# short lock — never one transaction spanning the scan, per
|
|
58
|
+
# principle 1. `value_expression` is the exact SQL
|
|
59
|
+
# `PostgresEra#query_expression(field)` already compiles for
|
|
60
|
+
# this field, applied against a `state` column this module's own
|
|
61
|
+
# SQL always makes available in scope (see `field_cache_source_sql`)
|
|
62
|
+
# — ONE source of truth for "what does this field mean", not a
|
|
63
|
+
# second hand-rolled copy that could silently drift from what a
|
|
64
|
+
# live query actually filters on.
|
|
65
|
+
def ensure_field_cache!(storage_name, era, field, value_expression)
|
|
66
|
+
name = field_cache(storage_name, era, field)
|
|
67
|
+
unless table_exists?(name)
|
|
68
|
+
nested_transaction("hecks_field_cache_create") do
|
|
69
|
+
@db.exec_params("SELECT pg_advisory_xact_lock(hashtext('hecks_field_cache:' || $1))", [name])
|
|
70
|
+
next if table_exists?(name)
|
|
71
|
+
|
|
72
|
+
@db.exec(<<~SQL)
|
|
73
|
+
CREATE TABLE #{quote(name)} (
|
|
74
|
+
id text PRIMARY KEY,
|
|
75
|
+
ordinal bigint NOT NULL,
|
|
76
|
+
value text
|
|
77
|
+
)
|
|
78
|
+
SQL
|
|
79
|
+
@db.exec("CREATE INDEX IF NOT EXISTS #{quote("#{name}_value_idx")} ON #{quote(name)} (value)")
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
backfill_field_cache!(name, storage_name, era, value_expression)
|
|
83
|
+
name
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def backfill_field_cache!(name, storage_name, era, value_expression)
|
|
87
|
+
chunked_backfill!(
|
|
88
|
+
name,
|
|
89
|
+
source_sql: lambda do |cursor|
|
|
90
|
+
<<~SQL
|
|
91
|
+
SELECT id, ordinal, value FROM (#{field_cache_source_sql(storage_name, era, value_expression)}) reduced
|
|
92
|
+
#{cursor ? "WHERE id > #{text_literal(cursor)}" : ''}
|
|
93
|
+
ORDER BY id LIMIT #{ResumableBackfill::CHUNK_SIZE}
|
|
94
|
+
SQL
|
|
95
|
+
end,
|
|
96
|
+
upsert: lambda do |rows|
|
|
97
|
+
upsert_field_cache_rows!(name, rows)
|
|
98
|
+
end
|
|
99
|
+
)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# THE LIVE-WRITE SIDE — called from `append`, inside the SAME
|
|
103
|
+
# transaction as the journal insert and the head-snapshot
|
|
104
|
+
# upsert, for every field this aggregate has a cache table for.
|
|
105
|
+
# `state_json` is the entry's OWN new state (already the thing
|
|
106
|
+
# `append` is about to write) — bound as a literal jsonb value
|
|
107
|
+
# and aliased `state`, so `value_expression` (built to read a
|
|
108
|
+
# column literally named `state`) evaluates identically here and
|
|
109
|
+
# in every backfill/query-time use, with no separate Ruby-side
|
|
110
|
+
# re-implementation of "how to pick a value-object's member" to
|
|
111
|
+
# keep in sync.
|
|
112
|
+
def upsert_field_cache_row!(name, id, ordinal, state_json, value_expression)
|
|
113
|
+
@db.exec_params(<<~SQL, [id, ordinal, state_json])
|
|
114
|
+
INSERT INTO #{quote(name)} (id, ordinal, value)
|
|
115
|
+
SELECT $1, $2, #{value_expression}
|
|
116
|
+
FROM (SELECT $3::jsonb AS state) src
|
|
117
|
+
ON CONFLICT (id) DO UPDATE SET ordinal = EXCLUDED.ordinal, value = EXCLUDED.value
|
|
118
|
+
WHERE #{quote(name)}.ordinal < EXCLUDED.ordinal
|
|
119
|
+
SQL
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def delete_field_cache_row!(name, id)
|
|
123
|
+
@db.exec_params("DELETE FROM #{quote(name)} WHERE id = $1", [id])
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
private
|
|
127
|
+
|
|
128
|
+
# The reduced (id, ordinal, state) source a field cache backfills
|
|
129
|
+
# from — NOT the raw journal (unlike `backfill_head_snapshot!`'s
|
|
130
|
+
# own source): a field cache reflects the CURRENT HEAD value,
|
|
131
|
+
# which for era > 1 can come from either this era's own live
|
|
132
|
+
# writes OR an untouched-this-era id whose value still comes
|
|
133
|
+
# from the translated ancestor tail. Era 1 has no ancestor tail
|
|
134
|
+
# (the snapshot table IS the head, verbatim); era N reduces the
|
|
135
|
+
# SAME two sources `compile_head!`'s own final view definition
|
|
136
|
+
# unions (head_compiler.rb) — this is deliberately the identical
|
|
137
|
+
# shape, ordinal kept in the SELECT list instead of dropped,
|
|
138
|
+
# because a backfilled cache row needs a REAL ordinal to guard
|
|
139
|
+
# against a concurrent live write the same way every other
|
|
140
|
+
# upsert in this adapter already does. Ordinals are safe to
|
|
141
|
+
# compare across that union because they come from ONE spanning
|
|
142
|
+
# sequence per domain (see lineage.rb's own header comment) —
|
|
143
|
+
# an ancestor-era ordinal is always numerically smaller than any
|
|
144
|
+
# current-era one, so the ordinary `ordinal <` guard already
|
|
145
|
+
# means the right thing with no era-aware special case.
|
|
146
|
+
def field_cache_source_sql(storage_name, era, value_expression)
|
|
147
|
+
era = era.to_i
|
|
148
|
+
if era == 1
|
|
149
|
+
return "SELECT id, ordinal, #{value_expression} AS value " \
|
|
150
|
+
"FROM #{quote(head_snapshot(storage_name, era))}"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
held = eras.find { |candidate| candidate[:ordinal] == era }
|
|
154
|
+
view = held && held[:label] && matview(storage_name, era, held[:label])
|
|
155
|
+
ancestor_side =
|
|
156
|
+
if view && view_exists?(view)
|
|
157
|
+
"SELECT ordinal, aggregate_id AS id, state FROM #{quote(view)} WHERE operation = 'save'"
|
|
158
|
+
else
|
|
159
|
+
# No compiled ancestor matview yet (e.g. this era was just
|
|
160
|
+
# minted and compile_head! hasn't run for THIS aggregate) —
|
|
161
|
+
# the live snapshot side alone is the whole truth so far.
|
|
162
|
+
"SELECT NULL::bigint AS ordinal, NULL::text AS id, NULL::jsonb AS state WHERE FALSE"
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
<<~SQL
|
|
166
|
+
SELECT id, ordinal, #{value_expression} AS value FROM (
|
|
167
|
+
SELECT DISTINCT ON (id) id, ordinal, state FROM (
|
|
168
|
+
#{ancestor_side}
|
|
169
|
+
UNION ALL
|
|
170
|
+
SELECT ordinal, id, state FROM #{quote(head_snapshot(storage_name, era))}
|
|
171
|
+
) merged ORDER BY id, ordinal DESC
|
|
172
|
+
) reduced
|
|
173
|
+
SQL
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def upsert_field_cache_rows!(name, rows)
|
|
177
|
+
rows.each do |row|
|
|
178
|
+
@db.exec_params(
|
|
179
|
+
"INSERT INTO #{quote(name)} (id, ordinal, value) VALUES ($1, $2, $3) " \
|
|
180
|
+
"ON CONFLICT (id) DO UPDATE SET ordinal = EXCLUDED.ordinal, value = EXCLUDED.value " \
|
|
181
|
+
"WHERE #{quote(name)}.ordinal < EXCLUDED.ordinal",
|
|
182
|
+
[row["id"], row["ordinal"], row["value"]]
|
|
183
|
+
)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|