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,51 @@
|
|
|
1
|
+
require_relative "../../ports/query/in_memory"
|
|
2
|
+
require_relative "../../ports/query/ordering"
|
|
3
|
+
require_relative "../../query_specification/common/order_by"
|
|
4
|
+
require_relative "../../query_specification/field_path"
|
|
5
|
+
require_relative "../../runtime/errors"
|
|
6
|
+
|
|
7
|
+
module Hecks
|
|
8
|
+
module Adapters
|
|
9
|
+
# SHARED BY every adapter that holds decoded Ruby records rather than
|
|
10
|
+
# running real SQL (Memory, Lambda, Heki) — the identical dotted-path
|
|
11
|
+
# value-object member-picking Postgres/Sqlite/D1's own
|
|
12
|
+
# order_expression does (numeric member wins, else the one-field
|
|
13
|
+
# convention "value"), just walked in Ruby instead of compiled to a
|
|
14
|
+
# JSONB/json_extract path, because there is no query engine
|
|
15
|
+
# underneath any of these three to hide that walk inside.
|
|
16
|
+
module InMemoryOrdering
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
# order_by IS A RUNTIME VALUE (an HTTP query param, in the
|
|
20
|
+
# console's case), not framework-authored bluebook source — see
|
|
21
|
+
# postgres.rb's own all for the full reasoning. Whitelisted the
|
|
22
|
+
# identical way before FieldPath.dig ever runs.
|
|
23
|
+
def ordered(records, aggregate:, order_by:, direction:)
|
|
24
|
+
return records unless order_by
|
|
25
|
+
|
|
26
|
+
name = order_by.to_s.split(".").first
|
|
27
|
+
raise Runtime::WiringError, "#{aggregate.name} has no attribute #{order_by.inspect} to order by" unless aggregate.lifecycle&.field.to_s == name || aggregate.attribute(name)
|
|
28
|
+
|
|
29
|
+
path = sortable_path(aggregate, order_by)
|
|
30
|
+
spec = QuerySpecification::Common::OrderBy.new(field: order_by, direction: direction)
|
|
31
|
+
Ports::Query::Ordering.apply(records, spec, nil, identity: ->(record) { record.id.to_s }) do |record|
|
|
32
|
+
Ports::Query::InMemory.comparable(QuerySpecification::FieldPath.dig(record, path))
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def sortable_path(aggregate, field)
|
|
37
|
+
name, *path = field.to_s.split(".")
|
|
38
|
+
return field.to_s unless path.empty?
|
|
39
|
+
|
|
40
|
+
attribute = aggregate.attribute(name)
|
|
41
|
+
return field.to_s if aggregate.lifecycle&.field.to_s == name || attribute.nil?
|
|
42
|
+
|
|
43
|
+
vo = aggregate.value_object(attribute.type)
|
|
44
|
+
return field.to_s unless vo
|
|
45
|
+
|
|
46
|
+
member = vo.attributes.find { |a| %w[Integer Float].include?(a.type) }&.name || "value"
|
|
47
|
+
"#{name}.#{member}"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Adapters
|
|
5
|
+
class Lambda
|
|
6
|
+
# THE THIN AWS CLIENT — shared by this adapter's own read methods
|
|
7
|
+
# (Lambda#find/#all/#count/#query) and Runtime::RemoteDispatcher's
|
|
8
|
+
# write methods. One Lambda invoke, one JSON round trip, nothing
|
|
9
|
+
# domain-specific: neither caller needs to know an AWS SDK is
|
|
10
|
+
# involved at all.
|
|
11
|
+
#
|
|
12
|
+
# FUNCTION NAME IS COMPUTED, NOT PASSED — `"hecks-#{domain}"`,
|
|
13
|
+
# lowercased, matches bin/project_deploy's own `stack_name`
|
|
14
|
+
# exactly (bin/project_deploy: `stack_name = "hecks-#{domain_name}"`,
|
|
15
|
+
# `domain_name = File.basename(domain)`). `domain` here is the
|
|
16
|
+
# bluebook's OWN declared name (`Embryonaut`, not the directory);
|
|
17
|
+
# today's real corpus has directory name == declared name
|
|
18
|
+
# lowercased for every domain that deploys, so `.downcase` alone
|
|
19
|
+
# reproduces the same string bin/project_deploy computes from the
|
|
20
|
+
# directory — this is a real, load-bearing assumption, not a
|
|
21
|
+
# coincidence to lean on silently forever if that ever stops
|
|
22
|
+
# holding.
|
|
23
|
+
class Client
|
|
24
|
+
def initialize(domain:, region:)
|
|
25
|
+
require "aws-sdk-lambda"
|
|
26
|
+
@function_name = "hecks-#{domain.to_s.downcase}"
|
|
27
|
+
@client = Aws::Lambda::Client.new(region: region)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# THE WHOLE DOMAIN, EVERY TIME — matches dispatch::read's own
|
|
31
|
+
# rehydrate-the-full-journal design (Phase 1, rust/host). No
|
|
32
|
+
# caching here, deliberately not even per-request: Lambda#all
|
|
33
|
+
# used to memoize this across calls, which silently served
|
|
34
|
+
# stale reads for a warm web process's whole lifetime once a
|
|
35
|
+
# write happened elsewhere — see Lambda#instances's own comment
|
|
36
|
+
# on the real, live bug that caught it.
|
|
37
|
+
def read
|
|
38
|
+
invoke({ "read" => true })
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def dispatch(verb, args, role: nil)
|
|
42
|
+
payload = { "verb" => verb, "args" => args }
|
|
43
|
+
payload["role"] = role if role
|
|
44
|
+
invoke(payload)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def invoke(payload)
|
|
50
|
+
response = @client.invoke(function_name: @function_name, payload: JSON.generate(payload))
|
|
51
|
+
body = JSON.parse(response.payload.read)
|
|
52
|
+
|
|
53
|
+
if response.function_error
|
|
54
|
+
raise Runtime::WiringError,
|
|
55
|
+
"Lambda #{@function_name} (#{response.function_error}): #{body['errorMessage'] || body}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
body
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
require_relative "lambda/client"
|
|
2
|
+
require_relative "../../ports/query/in_memory"
|
|
3
|
+
require_relative "../../ports/persistence/remote_runtime"
|
|
4
|
+
require_relative "in_memory_ordering"
|
|
5
|
+
require_relative "../../runtime/instance"
|
|
6
|
+
|
|
7
|
+
module Hecks
|
|
8
|
+
module Adapters
|
|
9
|
+
# THE READ-SIDE HALF OF LAMBDA ROUTING — `persisted_by("Lambda")`'s
|
|
10
|
+
# own adapter, resolved through the SAME `Ports::Persistence`
|
|
11
|
+
# machinery `persisted_by("Postgres")`/`persisted_by("Memory")`
|
|
12
|
+
# already use (`Runtime::Registry#repository`, `RepositoryFactory
|
|
13
|
+
# .build`) — no new framework plumbing, just a new adapter class at
|
|
14
|
+
# the name the existing lookup already expects. See
|
|
15
|
+
# `Runtime::RemoteDispatcher` for the write-side half; the two
|
|
16
|
+
# compose through one shared `Client` per domain rather than each
|
|
17
|
+
# inventing its own AWS wiring.
|
|
18
|
+
#
|
|
19
|
+
# READ-ONLY, DELIBERATELY — `append`/`project` raise rather than
|
|
20
|
+
# silently no-op. A write reaching this class would mean
|
|
21
|
+
# `Runtime::CommandInterpreter` ran locally against a Lambda-routed
|
|
22
|
+
# domain, which is exactly the bypass `Runtime::RemoteDispatcher`
|
|
23
|
+
# exists to prevent (business-rule validation must happen in Rust
|
|
24
|
+
# for these domains, not be pre-computed here against an
|
|
25
|
+
# incomplete local view). A loud failure here is the correct
|
|
26
|
+
# outcome, not a bug to route around.
|
|
27
|
+
class Lambda
|
|
28
|
+
include Ports::Persistence::RemoteRuntime
|
|
29
|
+
|
|
30
|
+
attr_reader :aggregate
|
|
31
|
+
|
|
32
|
+
def initialize(aggregate:, settings: {}, root: nil)
|
|
33
|
+
@aggregate = aggregate
|
|
34
|
+
domain = settings[:domain] || settings["domain"] || aggregate.name
|
|
35
|
+
region = settings[:region] || settings["region"] || "us-east-1"
|
|
36
|
+
# TWO DIFFERENT "domain"s, deliberately not conflated: `domain`
|
|
37
|
+
# (this aggregate's OWN bluebook name — "Identity", "Governance")
|
|
38
|
+
# only ever prefixes the instances lookup, since that's how
|
|
39
|
+
# rust/host's own `Store::instances()` keys every record
|
|
40
|
+
# (`registry.rb`'s own `"#{a[:domain_name]}::#{a[:name]}#"`
|
|
41
|
+
# dump format, unchanged by which chapter attached it). The
|
|
42
|
+
# FUNCTION to actually call is a different question: Governance
|
|
43
|
+
# and Identity aggregates are compiled into the ATTACHING
|
|
44
|
+
# domain's own Lambda (one merged `Store` per target — Phase 0's
|
|
45
|
+
# framework-bluebook work), never a function of their own, so
|
|
46
|
+
# `settings[:domain]` is the wrong signal for `Client.new`.
|
|
47
|
+
# `root` is the boot's own directory (`Registry#root`, shared by
|
|
48
|
+
# EVERY bluebook in one registry regardless of which one
|
|
49
|
+
# attached it) — `File.basename(root)` reproduces the exact
|
|
50
|
+
# same string `bin/project_deploy`'s own `stack_name` computes
|
|
51
|
+
# from the domain PATH, so the two can never name two
|
|
52
|
+
# different functions for the same deploy... on a LOCAL boot,
|
|
53
|
+
# where `root` is a real project directory. Inside the deployed
|
|
54
|
+
# Lambda itself `root` is ALWAYS `/var/task` (every Lambda's own
|
|
55
|
+
# fixed code root, regardless of domain) — `File.basename` gives
|
|
56
|
+
# "task", not the domain name, and invokes the wrong function
|
|
57
|
+
# entirely. A real, live AccessDeniedException on
|
|
58
|
+
# "hecks-task" caught this: invisible through every earlier
|
|
59
|
+
# phase's own verify step, all run from a local boot, until
|
|
60
|
+
# WebFunction became the first Ruby process to EVER make this
|
|
61
|
+
# exact call from inside a deployed Lambda. `DOMAIN_NAME` (set
|
|
62
|
+
# by bin/project_deploy's own WebFunction Environment) is the
|
|
63
|
+
# real, unambiguous signal in that specific context; the
|
|
64
|
+
# root-basename heuristic stays as the local-boot fallback,
|
|
65
|
+
# unchanged.
|
|
66
|
+
function_domain = ENV["DOMAIN_NAME"] || (root ? File.basename(root) : domain)
|
|
67
|
+
@client = Client.new(domain: function_domain, region: region)
|
|
68
|
+
@prefix = "#{domain}::#{aggregate.hecks_name}#"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def find(id)
|
|
72
|
+
instances[id.to_s]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def all(order_by: nil, direction: :asc)
|
|
76
|
+
InMemoryOrdering.ordered(instances.values, aggregate: @aggregate, order_by: order_by, direction: direction)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def count = instances.size
|
|
80
|
+
|
|
81
|
+
def query(specification, args = {}, context: {})
|
|
82
|
+
Ports::Query::InMemory.execute(instances.values, specification, args)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# `entries`/`append`/`project` — the append-only contract
|
|
86
|
+
# `Ports::Persistence::AppendOnly` requires of every adapter
|
|
87
|
+
# (raises at construction if any are missing). Answered by
|
|
88
|
+
# `Ports::Persistence::RemoteRuntime`, included above: `entries` is
|
|
89
|
+
# `[]` unconditionally (Lambda's own Postgres is already the
|
|
90
|
+
# durable store — rust/host's rehydrate-and-replay journal, Phase
|
|
91
|
+
# 1 — there is no local write-ahead log for `recover!` to replay),
|
|
92
|
+
# `append`/`project` raise rather than silently no-op.
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
# RE-FETCHED EVERY CALL, deliberately NOT memoized across them —
|
|
97
|
+
# this adapter itself is long-lived (one instance per aggregate,
|
|
98
|
+
# held by the registry `RUNTIME = Hecks.boot(...)` builds ONCE
|
|
99
|
+
# per Lambda web process — WebFunction's own top-level constant,
|
|
100
|
+
# reused warm across every HTTP request that process serves, not
|
|
101
|
+
# rebuilt per request the way a memoize-for-one-request comment
|
|
102
|
+
# here used to assume). A real, live bug caught this: a mutation
|
|
103
|
+
# dispatched fine (RemoteDispatcher always calls the dispatch
|
|
104
|
+
# Lambda fresh) and the very next `.all` on the SAME warm
|
|
105
|
+
# container kept returning the state from BEFORE that mutation,
|
|
106
|
+
# forever, until the container cold-started — memoizing here
|
|
107
|
+
# made every write invisible to every read on a warm container.
|
|
108
|
+
# Keyed by bare id (the part after "Domain::Aggregate#"), not the
|
|
109
|
+
# full "Domain::Aggregate#id" string — callers already know which
|
|
110
|
+
# aggregate they're asking this instance about.
|
|
111
|
+
def instances
|
|
112
|
+
@client.read.fetch("instances", {}).filter_map { |key, state|
|
|
113
|
+
next unless key.start_with?(@prefix)
|
|
114
|
+
|
|
115
|
+
[key.delete_prefix(@prefix), build_instance(key.delete_prefix(@prefix), state)]
|
|
116
|
+
}.to_h
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Lambda's own JSON response is already Ruby-decoded with STRING
|
|
120
|
+
# keys (plain `JSON.parse`, no `symbolize_names:`) — every other
|
|
121
|
+
# adapter's own `instance(row)` builder (Postgres, Sqlite) hands
|
|
122
|
+
# `Runtime::Instance.new` a SYMBOL-keyed state hash instead. A
|
|
123
|
+
# round trip through `JSON.generate`/`JSON.parse` is simpler and
|
|
124
|
+
# safer than a hand-rolled deep-symbolize helper for what is not
|
|
125
|
+
# a hot path.
|
|
126
|
+
def build_instance(id, state)
|
|
127
|
+
Runtime::Instance.new(aggregate: @aggregate, id: id, state: JSON.parse(JSON.generate(state), symbolize_names: true))
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
require_relative "../../ports/persistence/append_only"
|
|
2
|
+
require_relative "../../ports/query/in_memory"
|
|
3
|
+
require_relative "in_memory_ordering"
|
|
4
|
+
require_relative "../../runtime/instance"
|
|
5
|
+
|
|
6
|
+
module Hecks
|
|
7
|
+
module Adapters
|
|
8
|
+
class Memory
|
|
9
|
+
# TENANT-CAPABLE TRIVIALLY — see Runtime::TenantCheck's own header
|
|
10
|
+
# for the full reasoning. `@records` is a plain instance variable;
|
|
11
|
+
# two `Runtime.boot` calls build two entirely separate Registry
|
|
12
|
+
# objects and, through them, two entirely separate Memory
|
|
13
|
+
# instances, so two tenant boots never share this adapter's state
|
|
14
|
+
# by construction — nothing here needs to know "tenant" exists.
|
|
15
|
+
def self.tenant_capable? = true
|
|
16
|
+
def persistence_capabilities = [:atomic_put]
|
|
17
|
+
|
|
18
|
+
attr_reader :aggregate
|
|
19
|
+
|
|
20
|
+
def initialize(aggregate:, settings: {}, root: nil)
|
|
21
|
+
@aggregate = aggregate
|
|
22
|
+
@records = {}
|
|
23
|
+
@events = []
|
|
24
|
+
@entries = []
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def find(id) = @records[id.to_s]
|
|
28
|
+
def count = @records.size
|
|
29
|
+
|
|
30
|
+
def all(order_by: nil, direction: :asc)
|
|
31
|
+
InMemoryOrdering.ordered(@records.values, aggregate: @aggregate, order_by: order_by, direction: direction)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def query(specification, args = {}, context: {})
|
|
35
|
+
Ports::Query::InMemory.execute(all, specification, args, registry: context[:registry])
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def append(entry)
|
|
39
|
+
@entries << entry
|
|
40
|
+
entry
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def project(entry)
|
|
44
|
+
if entry.save?
|
|
45
|
+
@records[entry.id] = Runtime::Instance.new(aggregate: @aggregate, id: entry.id, state: entry.state.dup)
|
|
46
|
+
else
|
|
47
|
+
@records.delete(entry.id)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def save(instance)
|
|
52
|
+
entry = Ports::Persistence::Entry.new(operation: "save", id: instance.id.to_s, state: instance.state.dup)
|
|
53
|
+
append(entry)
|
|
54
|
+
project(entry)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# One in-memory critical section in the only thread touching this plain
|
|
58
|
+
# Hash: classify and replace without a preliminary repository lookup.
|
|
59
|
+
# Durable append and projection remain ordered exactly as ordinary save.
|
|
60
|
+
def atomic_put(entry, insert_only: false)
|
|
61
|
+
exists = @records.key?(entry.id.to_s)
|
|
62
|
+
return :conflicted if insert_only && exists
|
|
63
|
+
|
|
64
|
+
status = exists ? :replaced : :inserted
|
|
65
|
+
append(entry)
|
|
66
|
+
project(entry)
|
|
67
|
+
status
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def delete(id)
|
|
71
|
+
entry = Ports::Persistence::Entry.new(operation: "delete", id: id.to_s, state: nil)
|
|
72
|
+
append(entry)
|
|
73
|
+
project(entry)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def record_event(event) = @events << event
|
|
77
|
+
|
|
78
|
+
def events = @events
|
|
79
|
+
|
|
80
|
+
def entries = @entries.dup
|
|
81
|
+
|
|
82
|
+
# Every other driven adapter (Postgres/PostgresEra/Sqlite/D1)
|
|
83
|
+
# already implements this — `Ports::Persistence::AppendOnly#reset!`
|
|
84
|
+
# forwards to it and only raises when the wrapped adapter doesn't
|
|
85
|
+
# respond to `reset!` at all, which was always this adapter's own
|
|
86
|
+
# gap, not a deliberate omission (nothing about "in memory" implies
|
|
87
|
+
# "cannot be cleared"). Existing callers that fully re-`Hecks.boot`
|
|
88
|
+
# a domain per test case don't need this — they get a brand new
|
|
89
|
+
# `Memory` instance, with brand new empty `@records`/`@events`/
|
|
90
|
+
# `@entries`, for free. This is for the other case: a caller that
|
|
91
|
+
# deliberately keeps ONE booted runtime across many cases (to skip
|
|
92
|
+
# `load_domain`'s own per-boot parse/verify cost) and wants each
|
|
93
|
+
# case to start from the same clean slate `Hecks.boot` would have
|
|
94
|
+
# given it, without paying for a fresh boot to get there.
|
|
95
|
+
def reset!
|
|
96
|
+
@records = {}
|
|
97
|
+
@events = []
|
|
98
|
+
@entries = []
|
|
99
|
+
self
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Adapters
|
|
5
|
+
# A GENERIC STAND-IN for a real Checkout-Session-style adapter — no
|
|
6
|
+
# network call, no real account, matching whatever real payment
|
|
7
|
+
# adapter a project pairs it with by shape alone
|
|
8
|
+
# (`create_session(event:, registration_id:, success_url:, cancel_url:)
|
|
9
|
+
# -> a URL string`), the same way `Memory` stands in for a real
|
|
10
|
+
# persistence adapter without knowing anything about the domain bound
|
|
11
|
+
# to it. Never reads `event`'s own fields — a real adapter needs the
|
|
12
|
+
# price/name inside it to build a session a payer actually sees;
|
|
13
|
+
# this only needs to look enough like one to swap in.
|
|
14
|
+
#
|
|
15
|
+
# The URL it returns carries the same `registration_id` a real
|
|
16
|
+
# webhook's metadata would, so a caller building its own signed
|
|
17
|
+
# webhook against it (a smoke test, CI) has something real to key
|
|
18
|
+
# off of — the real shape production has, minus the network hop.
|
|
19
|
+
class MockStripeAdapter
|
|
20
|
+
def initialize(aggregate: nil, settings: {}, root: nil); end
|
|
21
|
+
|
|
22
|
+
def create_session(event:, registration_id:, success_url:, cancel_url:)
|
|
23
|
+
separator = success_url.include?("?") ? "&" : "?"
|
|
24
|
+
"#{success_url}#{separator}mock_checkout=1&mock_registration_id=#{registration_id}"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
require_relative "../../../runtime/value"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Adapters
|
|
5
|
+
class Postgres
|
|
6
|
+
# How a state crosses the column boundary: which fields persist, and
|
|
7
|
+
# how each one encodes into (and decodes out of) its column. Same
|
|
8
|
+
# SHAPE as Sqlite::Codec (one column per attribute, JSON for
|
|
9
|
+
# nested/list) — not its mechanics. Sqlite's own columns carry no
|
|
10
|
+
# type affinity worth trusting on the way back out (SQLite gives it
|
|
11
|
+
# back typed anyway via the driver), but `pg` hands every column
|
|
12
|
+
# back as TEXT unless a type map says otherwise, so a real `bigint`/
|
|
13
|
+
# `double precision` column needs its OWN coercion on decode that
|
|
14
|
+
# Sqlite never had to write.
|
|
15
|
+
module Codec
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def persisted_fields
|
|
19
|
+
fields = @aggregate.attributes.reject { |attribute| attribute.name == :id }.map do |attribute|
|
|
20
|
+
{ name: attribute.name, attribute: attribute, sql_type: sql_type(attribute) }
|
|
21
|
+
end
|
|
22
|
+
lifecycle = @aggregate.lifecycle
|
|
23
|
+
fields << { name: lifecycle.field, attribute: nil, sql_type: "text" } if lifecycle && !fields.any? { |field| field[:name] == lifecycle.field }
|
|
24
|
+
fields
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def encode(attr, value)
|
|
28
|
+
# NEVER SET IS NOT EMPTY — same reasoning as Sqlite::Codec's own
|
|
29
|
+
# comment: a list attribute nothing has ever appended to has to
|
|
30
|
+
# stay NULL to answer the same as Memory does, not become `[]`
|
|
31
|
+
# invented by this adapter's own storage.
|
|
32
|
+
return (value.nil? ? nil : JSON.generate(value)) if attr.list?
|
|
33
|
+
return JSON.generate(value) if value.is_a?(Hash) || value.is_a?(Runtime::Value)
|
|
34
|
+
|
|
35
|
+
value
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def encode_field(field, value)
|
|
39
|
+
return value unless field[:attribute]
|
|
40
|
+
|
|
41
|
+
encode(field[:attribute], value)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def decode(row)
|
|
45
|
+
persisted_fields.each_with_object({}) do |field, state|
|
|
46
|
+
attr = field[:attribute]
|
|
47
|
+
unless attr
|
|
48
|
+
state[field[:name]] = row[field[:name].to_s]
|
|
49
|
+
next
|
|
50
|
+
end
|
|
51
|
+
raw = row[attr.name.to_s]
|
|
52
|
+
state[attr.name] =
|
|
53
|
+
if attr.list? || value_object?(attr)
|
|
54
|
+
raw ? JSON.parse(raw, symbolize_names: true) : nil
|
|
55
|
+
else
|
|
56
|
+
coerce_scalar(attr, raw)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# `pg` returns every column as a Ruby String by default (no type
|
|
62
|
+
# map installed) — a `bigint`/`double precision` column has to be
|
|
63
|
+
# coerced back on the way out, unlike Sqlite3's own driver, which
|
|
64
|
+
# already types a row by the column's declared affinity. Mirrors
|
|
65
|
+
# SQL_TYPES' own mapping: whatever gets a real numeric column here
|
|
66
|
+
# gets converted back here, everything else (String, references,
|
|
67
|
+
# enums, ...) passes through as the text it already is.
|
|
68
|
+
def coerce_scalar(attr, raw)
|
|
69
|
+
return nil if raw.nil?
|
|
70
|
+
|
|
71
|
+
case attr.type
|
|
72
|
+
when "Integer" then raw.to_i
|
|
73
|
+
when "Float" then raw.to_f
|
|
74
|
+
else raw
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
require "digest"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Adapters
|
|
5
|
+
class Postgres
|
|
6
|
+
# The DDL: one table per aggregate, a real column per attribute (a
|
|
7
|
+
# jsonb column per nested/list attribute), an append-only entry
|
|
8
|
+
# table beside it, the shared events/saga tables — and the automatic
|
|
9
|
+
# indexing this adapter carries that `PostgresEra` does not (see
|
|
10
|
+
# docs/implemented/postgres-era-adapter-split-plan.md, governing principle 3:
|
|
11
|
+
# every field a declared query filters or sorts on is safe to index
|
|
12
|
+
# directly here, since there is no `head_view` reduction to route
|
|
13
|
+
# around the way `PostgresEra`'s own cache-table story exists for).
|
|
14
|
+
module SchemaBuilder
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def create_aggregate_table!
|
|
18
|
+
columns = persisted_fields.map { |field| "#{quote_ident(field[:name])} #{field[:sql_type]}" }
|
|
19
|
+
@db.exec(
|
|
20
|
+
"CREATE TABLE IF NOT EXISTS #{quoted_table} (id text PRIMARY KEY#{columns.empty? ? '' : ', '}#{columns.join(', ')})"
|
|
21
|
+
)
|
|
22
|
+
# RIGHT HERE, NOT A SEPARATE STEP IN `Postgres#initialize` —
|
|
23
|
+
# same idiom Sqlite::SchemaBuilder's own `create_aggregate_table!`
|
|
24
|
+
# uses: index creation runs unconditionally, right after the
|
|
25
|
+
# table it indexes exists, the same self-healing shape
|
|
26
|
+
# `ensure_head_snapshot!`/`ensure_first_head!` already use on
|
|
27
|
+
# `PostgresEra`'s side.
|
|
28
|
+
ensure_indexes!
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def create_entry_table!
|
|
32
|
+
@db.exec(<<~SQL)
|
|
33
|
+
CREATE TABLE IF NOT EXISTS #{quoted_entry_table} (
|
|
34
|
+
sequence bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
35
|
+
aggregate_id text NOT NULL,
|
|
36
|
+
operation text NOT NULL DEFAULT 'save',
|
|
37
|
+
state jsonb NOT NULL,
|
|
38
|
+
mirrors jsonb
|
|
39
|
+
)
|
|
40
|
+
SQL
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Same DDL as `PostgresEra`'s own — not lineage-specific, copied
|
|
44
|
+
# verbatim (see docs/implemented/postgres-era-adapter-split-plan.md).
|
|
45
|
+
def create_event_table!
|
|
46
|
+
@db.exec(<<~SQL)
|
|
47
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
48
|
+
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
49
|
+
name text NOT NULL,
|
|
50
|
+
aggregate text NOT NULL,
|
|
51
|
+
aggregate_id text NOT NULL,
|
|
52
|
+
payload jsonb,
|
|
53
|
+
occurred_at text
|
|
54
|
+
)
|
|
55
|
+
SQL
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Same DDL as `PostgresEra`'s own — not lineage-specific, copied
|
|
59
|
+
# verbatim (see docs/implemented/postgres-era-adapter-split-plan.md).
|
|
60
|
+
def create_saga_table!
|
|
61
|
+
@db.exec(<<~SQL)
|
|
62
|
+
CREATE TABLE IF NOT EXISTS hecks_saga_instances (
|
|
63
|
+
domain text NOT NULL,
|
|
64
|
+
process_manager text NOT NULL,
|
|
65
|
+
correlation text NOT NULL,
|
|
66
|
+
state text NOT NULL,
|
|
67
|
+
memory jsonb NOT NULL,
|
|
68
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
69
|
+
PRIMARY KEY (domain, process_manager, correlation)
|
|
70
|
+
)
|
|
71
|
+
SQL
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def sql_type(attr)
|
|
75
|
+
return "jsonb" if attr.list? || value_object?(attr)
|
|
76
|
+
|
|
77
|
+
SQL_TYPES.fetch(attr.type, "text")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# ── automatic indexing ───────────────────────────────────────
|
|
81
|
+
#
|
|
82
|
+
# Derived from every declared query's `where`/`order_by` fields —
|
|
83
|
+
# no bluebook author opts in, matching every other self-healing
|
|
84
|
+
# schema move this adapter makes. Walks the aggregate's own
|
|
85
|
+
# queries AND each entity's own queries — the same enumeration
|
|
86
|
+
# `dsl/aggregate_builder.rb`'s own `query_surfaces` walks at
|
|
87
|
+
# declaration-seal time — but an entity's own fields never
|
|
88
|
+
# produce an index below: an entity has no table of its own, its
|
|
89
|
+
# rows live inside a LIST-typed attribute on the AGGREGATE's own
|
|
90
|
+
# table (`QueryInterpreter#entity_rows` resolves it that way, and
|
|
91
|
+
# answers an entity query entirely in memory over `repository.all`
|
|
92
|
+
# — it never reaches this adapter's own `query` method at all).
|
|
93
|
+
# Every list-typed attribute is excluded below regardless (see
|
|
94
|
+
# `index_field!`), so an entity's own field would always resolve
|
|
95
|
+
# to "skip" anyway. Walked explicitly rather than silently
|
|
96
|
+
# dropped, so a reader can see entities were considered here, not
|
|
97
|
+
# forgotten.
|
|
98
|
+
def ensure_indexes!
|
|
99
|
+
query_surfaces.each do |owner, queries|
|
|
100
|
+
next unless owner.equal?(@aggregate)
|
|
101
|
+
|
|
102
|
+
queries.each { |query| index_query!(query) }
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def query_surfaces
|
|
107
|
+
[[@aggregate, @aggregate.queries]] +
|
|
108
|
+
@aggregate.entities.map { |entity| [entity, entity.queries] }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def index_query!(query)
|
|
112
|
+
query.wheres.each { |clause| index_field!(clause.field) }
|
|
113
|
+
index_field!(query.order_by.field) if query.order_by
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# A `list_of` attribute is never indexed here — this adapter's
|
|
117
|
+
# own `contains` on a list field compiles to `EXISTS (SELECT 1
|
|
118
|
+
# FROM jsonb_array_elements(...) ...)` (see `list_contains_clause`
|
|
119
|
+
# below), a SQL shape neither a plain btree on the raw jsonb
|
|
120
|
+
# column nor even a GIN jsonb index (`@>`, `?`) accelerates —
|
|
121
|
+
# Postgres's own GIN jsonb operators match a DIFFERENT SQL shape
|
|
122
|
+
# than the `EXISTS` + `jsonb_array_elements` this codebase always
|
|
123
|
+
# compiles a list `contains` to. An index here would be dead
|
|
124
|
+
# weight, not free correctness, so none is attempted.
|
|
125
|
+
def index_field!(field)
|
|
126
|
+
name, *_path = field.to_s.split(".")
|
|
127
|
+
attribute = @aggregate.attribute(name)
|
|
128
|
+
return if attribute&.list?
|
|
129
|
+
|
|
130
|
+
# THE SAME EXPRESSION THE QUERY ITSELF COMPILES TO — calling
|
|
131
|
+
# `query_expression`/`plain_column`, the real dialect methods,
|
|
132
|
+
# rather than a second, hand-rolled derivation of the same
|
|
133
|
+
# path that could silently drift from it. An index whose
|
|
134
|
+
# expression doesn't match the planner's own candidate
|
|
135
|
+
# expression byte-for-byte is invisible to the planner,
|
|
136
|
+
# created or not.
|
|
137
|
+
expression = query_expression(field.to_s)
|
|
138
|
+
if expression == plain_column(name)
|
|
139
|
+
create_plain_index!(name)
|
|
140
|
+
else
|
|
141
|
+
create_expression_index!(expression, field.to_s)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def create_plain_index!(column)
|
|
146
|
+
name = index_name(column.to_s)
|
|
147
|
+
@db.exec("CREATE INDEX IF NOT EXISTS #{quote_ident(name)} ON #{quoted_table} (#{quote_ident(column)})")
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def create_expression_index!(expression, field)
|
|
151
|
+
name = index_name(field)
|
|
152
|
+
@db.exec("CREATE INDEX IF NOT EXISTS #{quote_ident(name)} ON #{quoted_table} ((#{expression}))")
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Postgres identifiers cap at 63 bytes. Hashing TABLE + FIELD
|
|
156
|
+
# together keeps every generated index name well under that no
|
|
157
|
+
# matter how long an aggregate or attribute name gets, and keeps
|
|
158
|
+
# two distinct fields — on the same table or different ones —
|
|
159
|
+
# from ever colliding, or truncating into the same name the way
|
|
160
|
+
# a naive `"idx_#{table}_#{field}"` could once either got long
|
|
161
|
+
# enough.
|
|
162
|
+
def index_name(field)
|
|
163
|
+
"hecks_idx_#{Digest::SHA256.hexdigest("#{table}:#{field}")[0, 40]}"
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|