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,193 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
require_relative "era_guard/shape_diff"
|
|
3
|
+
require_relative "../bluebook/dsl/malformed"
|
|
4
|
+
require_relative "../bluebook/meta_validator"
|
|
5
|
+
require_relative "../naming"
|
|
6
|
+
require_relative "../ports/loading"
|
|
7
|
+
require_relative "../ports/persistence/lineage"
|
|
8
|
+
require_relative "era_check"
|
|
9
|
+
require_relative "registry"
|
|
10
|
+
|
|
11
|
+
module Hecks
|
|
12
|
+
module Runtime
|
|
13
|
+
# Detects when a bluebook's storage shape has drifted from what its data
|
|
14
|
+
# was written under — a rename or restructure with nothing to explain
|
|
15
|
+
# it. Refuses at boot, before a command can run into the gap and raise
|
|
16
|
+
# something that only makes sense once you already suspect a rename.
|
|
17
|
+
#
|
|
18
|
+
# The held source is a snapshot of the bluebook text as it stood the
|
|
19
|
+
# last time its shape was accepted. It updates only when a drift is
|
|
20
|
+
# seen AND covered by a translation — never on every boot, and never
|
|
21
|
+
# silently when a rename has nothing to explain it.
|
|
22
|
+
module EraGuard
|
|
23
|
+
extend ShapeDiff
|
|
24
|
+
|
|
25
|
+
module_function
|
|
26
|
+
|
|
27
|
+
def check!(registry, directory)
|
|
28
|
+
registry.bluebooks.each_value { |bluebook| check_bluebook!(registry, bluebook, directory) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def check_bluebook!(registry, bluebook, directory)
|
|
32
|
+
current_text = EraCheck.source_text_for(bluebook, directory)
|
|
33
|
+
return unless current_text
|
|
34
|
+
|
|
35
|
+
era_dir = File.join(registry.root, "data", "eras")
|
|
36
|
+
held_path = File.join(era_dir, "#{Naming.snake(bluebook.name)}.bluebook")
|
|
37
|
+
|
|
38
|
+
FileUtils.mkdir_p(era_dir)
|
|
39
|
+
unless File.exist?(held_path)
|
|
40
|
+
File.write(held_path, current_text)
|
|
41
|
+
return
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
held_bluebook = shadow_parse(File.read(held_path, encoding: "UTF-8"), held_path)
|
|
45
|
+
drifted = false
|
|
46
|
+
|
|
47
|
+
bluebook.aggregates.each do |aggregate|
|
|
48
|
+
lineage = Ports::Persistence::Lineage.for(registry, bluebook.name, aggregate)
|
|
49
|
+
held_aggregate = held_bluebook.aggregate(lineage&.ancestor_name || aggregate.name)
|
|
50
|
+
next unless held_aggregate
|
|
51
|
+
next if shape(aggregate) == shape(held_aggregate)
|
|
52
|
+
|
|
53
|
+
drifted = true
|
|
54
|
+
uncovered = uncovered_attributes(aggregate, held_aggregate, lineage)
|
|
55
|
+
refuse_uncovered!(bluebook, aggregate, uncovered) unless uncovered.empty?
|
|
56
|
+
|
|
57
|
+
unsafe = unsafe_additions(aggregate, held_aggregate, lineage)
|
|
58
|
+
refuse_unsafe_addition!(bluebook, aggregate, unsafe) unless unsafe.empty?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
check_vanished_aggregates!(registry, bluebook, held_bluebook)
|
|
62
|
+
File.write(held_path, current_text) if drifted
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# An aggregate that existed in the held text and answers to no
|
|
66
|
+
# current name — renamed silently, with nothing declaring `was:` to
|
|
67
|
+
# explain where its data went — is exactly the disease this guards
|
|
68
|
+
# against, and a plain per-aggregate diff would never see it: the
|
|
69
|
+
# current aggregate simply has no held counterpart to compare to.
|
|
70
|
+
def check_vanished_aggregates!(registry, bluebook, held_bluebook)
|
|
71
|
+
held_bluebook.aggregates.each do |held_aggregate|
|
|
72
|
+
claimed = bluebook.aggregates.any? do |aggregate|
|
|
73
|
+
aggregate.name == held_aggregate.name ||
|
|
74
|
+
registry.translations.any? do |translation|
|
|
75
|
+
translation.domain == bluebook.name &&
|
|
76
|
+
translation.for_aggregate(aggregate.name)&.was == held_aggregate.name
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
claimed ||= registry.translations.any? do |translation|
|
|
80
|
+
translation.domain == bluebook.name && translation.retired.include?(held_aggregate.name)
|
|
81
|
+
end
|
|
82
|
+
next if claimed
|
|
83
|
+
|
|
84
|
+
raise WiringError,
|
|
85
|
+
"cannot boot #{bluebook.name}: #{held_aggregate.name} existed and now doesn't, " \
|
|
86
|
+
"and nothing declares was: #{held_aggregate.name.inspect} to explain where its data went."
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# The Layer-1 coverage refusal — one wording, whoever detects the
|
|
91
|
+
# gap (the file-store boot check here, or Postgres's mint path).
|
|
92
|
+
def refuse_uncovered!(bluebook, aggregate, uncovered)
|
|
93
|
+
raise WiringError,
|
|
94
|
+
"cannot boot #{bluebook.name}::#{aggregate.name}: its shape changed and " \
|
|
95
|
+
"#{uncovered.map { |path| render_path(path) }.join(', ')} #{uncovered.size == 1 ? 'is' : 'are'} not " \
|
|
96
|
+
"explained by any rename, move, convert, retype, or drop. Update bluebook/translations/*.bluebook, e.g. " \
|
|
97
|
+
"#{suggestion(uncovered.first)}."
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# The addition-side sibling of refuse_uncovered! above — same
|
|
101
|
+
# wording shape, different cause: nothing vanished or changed type,
|
|
102
|
+
# something new arrived that an existing record has no way to hold.
|
|
103
|
+
def refuse_unsafe_addition!(bluebook, aggregate, unsafe)
|
|
104
|
+
raise WiringError,
|
|
105
|
+
"cannot boot #{bluebook.name}::#{aggregate.name}: #{unsafe.map { |name| ":#{name}" }.join(', ')} " \
|
|
106
|
+
"#{unsafe.size == 1 ? 'is new and required' : 'are new and required'}, with no default: to fill " \
|
|
107
|
+
"an existing record and no translation explaining what one should read there. Give it a " \
|
|
108
|
+
"default:, make it optional: true or list_of, or declare bluebook/translations/*.bluebook, e.g. " \
|
|
109
|
+
"`backfill :#{unsafe.first}, default: ...`."
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def render_path(path) = path.include?(".") ? path.inspect : ":#{path}"
|
|
113
|
+
|
|
114
|
+
def suggestion(path)
|
|
115
|
+
if path.include?(".")
|
|
116
|
+
"`move #{path.inspect}, to: #{path.inspect}` or `drop #{path.inspect}`"
|
|
117
|
+
else
|
|
118
|
+
"`rename :#{path}, to: :new_name` or `drop :#{path}`"
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Parses held source into its own IR, in a scratch registry so a past
|
|
123
|
+
# era's text never touches the one actually booting.
|
|
124
|
+
#
|
|
125
|
+
# NORMAL PARSE FIRST, shadow only as a FALLBACK — not shadow-parsing
|
|
126
|
+
# unconditionally, which is what this used to do. A handful of DSL
|
|
127
|
+
# defaults fork on `MetaValidator.shadow_parsing?` for a reason
|
|
128
|
+
# that has NOTHING to do with syntax the live grammar can no longer
|
|
129
|
+
# read at all (`identified_by { }`, `belongs_to`, `has_one`,
|
|
130
|
+
# `has_many` — genuinely removed spellings, exactly what shadow-
|
|
131
|
+
# parsing exists to keep readable): `reference_to`'s own default
|
|
132
|
+
# mint name (`default_reference_name`, attribute_collector.rb)
|
|
133
|
+
# changed from `_id`-suffixed to bare under ADR 0025, and THAT fork
|
|
134
|
+
# applies even to text using nothing but current, live syntax.
|
|
135
|
+
# Held text minted under the CURRENT grammar — every real era in
|
|
136
|
+
# this corpus today, since nothing has ever minted a second one —
|
|
137
|
+
# parses fine normally; only the reference-naming DEFAULT differed
|
|
138
|
+
# once shadow mode engaged unconditionally, so it silently
|
|
139
|
+
# reconstructed a DIFFERENT shape (and hash) than a fresh parse of
|
|
140
|
+
# the identical text — the same text hashing two different ways
|
|
141
|
+
# depending on which code path read it, breaking `ensure_named!`'s
|
|
142
|
+
# own from/to edge lookup with a spurious "no translation edge
|
|
143
|
+
# covers it" refusal that has nothing to do with any real
|
|
144
|
+
# translation gap.
|
|
145
|
+
#
|
|
146
|
+
# A normal parse can only ever SUCCEED on text the live grammar
|
|
147
|
+
# fully understands — there is no way for it to silently produce a
|
|
148
|
+
# wrong-but-plausible answer for genuinely legacy text, since every
|
|
149
|
+
# removed spelling refuses loudly (`Malformed`) rather than
|
|
150
|
+
# degrading. So: try normal first — if the ordinary grammar reads
|
|
151
|
+
# this text without complaint, that IS the canonical, unambiguous
|
|
152
|
+
# interpretation, the same one `label_of`/`mint_hash` on the same
|
|
153
|
+
# source text always computes, whoever's asking. Only on a
|
|
154
|
+
# `Malformed` refusal — the one signal that actually means "this
|
|
155
|
+
# spelling doesn't exist anymore" — fall back to the legacy
|
|
156
|
+
# grammar, exactly as before this change. Any OTHER exception (a
|
|
157
|
+
# genuine syntax error, an unrelated validation refusal) propagates
|
|
158
|
+
# unchanged; swallowing it here to retry under shadow mode would
|
|
159
|
+
# risk masking a real defect in the held text behind a confusing
|
|
160
|
+
# second failure instead of the original, more specific one.
|
|
161
|
+
#
|
|
162
|
+
# `MetaValidator.while_shadow_parsing` (ADR 0025, docs/dsl-work-
|
|
163
|
+
# slices.md's S0a) is what makes the fallback a LEGACY grammar
|
|
164
|
+
# rather than just a second copy of today's: it stops
|
|
165
|
+
# `BluebookBuilder.build` from judging this text against the
|
|
166
|
+
# grammar as it stands NOW, which is the one thing that would make
|
|
167
|
+
# a removed spelling refuse HISTORY the day it is removed from
|
|
168
|
+
# live source. The scratch registry is throwaway either way —
|
|
169
|
+
# nothing here is dispatched against or exposed to the real one —
|
|
170
|
+
# so skipping the judge/assemble round-trip changes nothing this
|
|
171
|
+
# method reads: `shape`, `uncovered_attributes`, and friends only
|
|
172
|
+
# ever ask the built IR for its own structure.
|
|
173
|
+
def shadow_parse(source, path)
|
|
174
|
+
parse_bluebook(source, path, shadow: false)
|
|
175
|
+
rescue Hecks::Bluebook::DSL::Malformed
|
|
176
|
+
parse_bluebook(source, path, shadow: true)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def parse_bluebook(source, path, shadow:)
|
|
180
|
+
scratch = Registry.new
|
|
181
|
+
loading = Ports::Loading.bootstrap
|
|
182
|
+
run = lambda do
|
|
183
|
+
Hecks.with_registry(scratch) do
|
|
184
|
+
loading.load_library
|
|
185
|
+
Kernel.eval(source, TOPLEVEL_BINDING, path, 1)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
shadow ? Hecks::Bluebook::MetaValidator.while_shadow_parsing(&run) : run.call
|
|
189
|
+
scratch.bluebooks.values.first
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "tempfile"
|
|
3
|
+
require_relative "era_guard"
|
|
4
|
+
require_relative "storage_shape"
|
|
5
|
+
|
|
6
|
+
module Hecks
|
|
7
|
+
module Runtime
|
|
8
|
+
# The refusal wording for a held era whose digest no longer matches its
|
|
9
|
+
# frozen text — the digest mismatch alone is what DETECTS tampering (a
|
|
10
|
+
# plain SHA256 comparison over raw bytes, unrelated to any of this);
|
|
11
|
+
# this only supplies the wording once that's already fired.
|
|
12
|
+
#
|
|
13
|
+
# LIVES HERE, NOT UNDER `Ports::Persistence` — `project` below directly
|
|
14
|
+
# calls `Runtime::EraGuard.shadow_parse`/`Runtime::StorageShape.project`
|
|
15
|
+
# (DSL-execution machinery), the "an adapter/port reaches into the
|
|
16
|
+
# runtime instead of being handed already-computed data" shape that
|
|
17
|
+
# has caused trouble here before. A capability this dependent on the
|
|
18
|
+
# runtime is a runtime-owned one that the Postgres adapter CALLS, not
|
|
19
|
+
# a ports-level module that happens to reach sideways into it.
|
|
20
|
+
#
|
|
21
|
+
# Used to also distinguish a cosmetic edit from a real shape change
|
|
22
|
+
# here, by re-parsing the edited text — a pure quality-of-message
|
|
23
|
+
# nicety, not a safety property, since dropped: every tamper refusal
|
|
24
|
+
# reaches the same generic wording now, the one this already fell to
|
|
25
|
+
# whenever it couldn't classify an edit anyway. An operator judges
|
|
26
|
+
# "did this matter" themselves,
|
|
27
|
+
# reading the still-archived original — an anomalous recovery moment
|
|
28
|
+
# already, not a normal boot path.
|
|
29
|
+
module EraTamper
|
|
30
|
+
module_function
|
|
31
|
+
|
|
32
|
+
def refusal(domain:, ordinal:)
|
|
33
|
+
"cannot boot #{domain}: the held text of era #{ordinal} was edited after it was frozen — " \
|
|
34
|
+
"held era texts are storage facts; restore the original text, or reset the data"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# The storage-shape projection of a bluebook text, JSON-normalized
|
|
38
|
+
# for structural comparison against a stored projection; nil when
|
|
39
|
+
# the text does not load. ALWAYS parsed through a fresh tempfile,
|
|
40
|
+
# never the held file's own path: the predicate extractor caches
|
|
41
|
+
# source by path, and a held path whose content has changed (the
|
|
42
|
+
# very situation this module exists for) would hand it stale
|
|
43
|
+
# lines.
|
|
44
|
+
def project(text, _source_path = nil)
|
|
45
|
+
file = Tempfile.new(["hecks-tamper-", ".bluebook"])
|
|
46
|
+
begin
|
|
47
|
+
file.write(text)
|
|
48
|
+
file.flush
|
|
49
|
+
bluebook = EraGuard.shadow_parse(text, file.path)
|
|
50
|
+
ensure
|
|
51
|
+
file.close!
|
|
52
|
+
end
|
|
53
|
+
return nil unless bluebook
|
|
54
|
+
|
|
55
|
+
JSON.parse(JSON.generate(StorageShape.project(bluebook)))
|
|
56
|
+
rescue StandardError, SyntaxError
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
require_relative "value/invariant_violation"
|
|
2
|
+
require_relative "../vocabulary"
|
|
3
|
+
|
|
4
|
+
module Hecks
|
|
5
|
+
module Runtime
|
|
6
|
+
class UnknownVerb < StandardError; end
|
|
7
|
+
class EnsuresNotMet < StandardError; end
|
|
8
|
+
class GivenNotMet < StandardError; end
|
|
9
|
+
class NotFound < StandardError; end
|
|
10
|
+
class LifecycleRefused < StandardError; end
|
|
11
|
+
class TypeMismatch < StandardError; end
|
|
12
|
+
# An argument the command does not declare. Sibling of TypeMismatch : that one
|
|
13
|
+
# is the right name carrying the wrong thing, this one is a name the command
|
|
14
|
+
# never had. Both are the payload gate refusing before any rule runs.
|
|
15
|
+
class UnknownArgument < StandardError; end
|
|
16
|
+
# The third of the trio, and the one that was missing : a name the command DOES
|
|
17
|
+
# declare, absent. TypeMismatch is the right name carrying the wrong thing,
|
|
18
|
+
# UnknownArgument a name that was never declared, AbsentArgument a declared name
|
|
19
|
+
# that never arrived. Between them they say a command takes exactly the
|
|
20
|
+
# arguments it declares — no others, and all of them.
|
|
21
|
+
class AbsentArgument < StandardError; end
|
|
22
|
+
# A creating command whose derived identity already names a record. The
|
|
23
|
+
# record it would have overwritten stands ; the dispatch that tried to
|
|
24
|
+
# mint a second one over it is what refuses.
|
|
25
|
+
class AlreadyExists < StandardError; end
|
|
26
|
+
# A declared, non-optional attribute a record predates — added since
|
|
27
|
+
# it was written, with no default: to fill it and no translation
|
|
28
|
+
# declaring what an old record should read there. `GuardState`
|
|
29
|
+
# (command_rules/admissibility.rb) is the one place this is raised: a
|
|
30
|
+
# `given`/`ensures`/`invariant` that reads the field would otherwise
|
|
31
|
+
# evaluate against a value nobody wrote, which is the same silent-
|
|
32
|
+
# wrong-answer class as an unpopulated projection reading "not
|
|
33
|
+
# active" (ADR 0025, "Added attributes and absence"). An OPTIONAL
|
|
34
|
+
# attribute in the same spot reads nil instead — that is what
|
|
35
|
+
# optional means, and this refusal is deliberately narrower than the
|
|
36
|
+
# nil-read it sits beside, not a replacement for it.
|
|
37
|
+
class AttributeAbsent < StandardError; end
|
|
38
|
+
# THE SAME SILENT-WRONG-ANSWER CLASS AS ABOVE, one line up — a
|
|
39
|
+
# `projects` field (S12, ADR 0025) this record predates, or that no
|
|
40
|
+
# rebuild sweep has populated yet, read by a `given`/`ensures`/
|
|
41
|
+
# `invariant` as though it carried a real value. `GuardState` is
|
|
42
|
+
# the one place this is raised, the same way AttributeAbsent is —
|
|
43
|
+
# a DECLARED field the record does not yet carry, distinguished
|
|
44
|
+
# from that one only in WHY: an ordinary attribute is absent
|
|
45
|
+
# because nobody backfilled it, a projected field is absent
|
|
46
|
+
# because nobody has swept it yet.
|
|
47
|
+
class ProjectionAbsent < StandardError; end
|
|
48
|
+
# A query or read model declares `authorize policy, tenant: :field` and
|
|
49
|
+
# the caller did not pass that field — the one half of `authorize` this
|
|
50
|
+
# runtime can enforce without a caller-identity system: the boundary
|
|
51
|
+
# itself, not whether the caller actually holds `policy`. See
|
|
52
|
+
# Runtime::TenantScope.
|
|
53
|
+
class Unauthorized < StandardError; end
|
|
54
|
+
|
|
55
|
+
# A Lambda-routed domain's own refusal (rust/host, `Runtime::
|
|
56
|
+
# RemoteDispatcher`), carrying Rust's own refusal text verbatim —
|
|
57
|
+
# NOT yet mapped back to the specific matching class above
|
|
58
|
+
# (GivenNotMet vs. EnsuresNotMet vs. ...), a real, known,
|
|
59
|
+
# documented gap: the WASM projector's own event/refusal-wording
|
|
60
|
+
# parity work (ADR 0021) makes the TEXT match Ruby's, but nothing
|
|
61
|
+
# yet parses that text back into a typed Ruby exception the way a
|
|
62
|
+
# local dispatch already raises one directly. Callers that only
|
|
63
|
+
# need "the domain said no" (not which specific rule) are
|
|
64
|
+
# unaffected; callers pattern-matching a SPECIFIC refusal class
|
|
65
|
+
# against a Lambda-routed domain are the ones this gap would bite.
|
|
66
|
+
class RemoteRefusal < StandardError; end
|
|
67
|
+
|
|
68
|
+
# The domain saying NO — the errors a reaction may legitimately meet and
|
|
69
|
+
# record as an undelivered outcome. A policy whose target refuses is a fact
|
|
70
|
+
# about the domain ; the originating command still stands.
|
|
71
|
+
#
|
|
72
|
+
# Everything ELSE is a defect : a NoMethodError in an interpreter, a
|
|
73
|
+
# NameError from a missing constant, a TypeError from a bad assumption. A
|
|
74
|
+
# blanket `rescue StandardError` used to fold both into one line —
|
|
75
|
+
# `delivered: false, reason: "..."` — so a crash in the runtime was
|
|
76
|
+
# indistinguishable from a rule doing its job, and read as normal operation
|
|
77
|
+
# in the log.
|
|
78
|
+
#
|
|
79
|
+
# UnknownVerb IS one of these, and deliberately : a cross-domain policy
|
|
80
|
+
# (`across "Notifications"`) fires in deployments where that domain is not
|
|
81
|
+
# loaded, and recording the undelivered reaction rather than raising is the
|
|
82
|
+
# design — spec/policy_spec states it in so many words, "records a reaction
|
|
83
|
+
# it cannot deliver rather than swallowing it".
|
|
84
|
+
# InvariantViolation belongs here and was missing. A value object refusing
|
|
85
|
+
# its own rule is the domain saying no as plainly as a given is — but the
|
|
86
|
+
# class is declared over in value.rb and never made the list, so the policy
|
|
87
|
+
# and saga interpreters, which rescue exactly these, would let it propagate
|
|
88
|
+
# as though the RUNTIME had broken. A reaction whose target violates an
|
|
89
|
+
# invariant is declined, not crashed. Found by spec/domain_refusal_spec on
|
|
90
|
+
# its first run : every corpus refusal must be a class named here, and 23
|
|
91
|
+
# of banking's were InvariantViolation.
|
|
92
|
+
# THE NAMES COME FROM THE LANGUAGE, the classes from this module.
|
|
93
|
+
# `DomainRefusal` declares WHICH refusals are the domain's own —
|
|
94
|
+
# a rule the caller broke — as against a runtime fault. Resolving
|
|
95
|
+
# each name here means a refusal declared but never defined fails
|
|
96
|
+
# at load with a NameError, rather than being quietly absent from
|
|
97
|
+
# a list nothing re-checks.
|
|
98
|
+
DOMAIN_REFUSALS = Hecks::Vocabulary.fetch("DomainRefusal").map { |name| const_get(name) }.freeze
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
require "time"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Runtime
|
|
5
|
+
# `correlation` is NOT on the wire — `to_h` below deliberately omits it,
|
|
6
|
+
# the same as `bin/run`'s own event projection does. It is runtime
|
|
7
|
+
# bookkeeping stamped by `Dispatcher#dispatch` when a saga leg's own
|
|
8
|
+
# dispatch causes this event (see `SagaInterpreter#deliver_saga_dispatch`
|
|
9
|
+
# and `#saga_correlation`) : a Hash of `correlation_head` -> correlation
|
|
10
|
+
# value, so an event caused by one saga cannot be misread by an unrelated
|
|
11
|
+
# one correlating on a different field. Absent for any event no saga
|
|
12
|
+
# dispatch caused, which is most of them.
|
|
13
|
+
Event = Struct.new(:name, :aggregate, :id, :payload, :occurred_at, :correlation, keyword_init: true) do
|
|
14
|
+
# AN EMITTED EVENT IS A RECORD OF SOMETHING THAT HAPPENED, and a
|
|
15
|
+
# mutable audit trail is not one. The PAYLOAD — the domain fact the
|
|
16
|
+
# event carries — is frozen THROUGH on emission: freezing the Hash
|
|
17
|
+
# alone would leave every value in it editable in place, which is
|
|
18
|
+
# the shape all four previous freezing bugs had.
|
|
19
|
+
#
|
|
20
|
+
# THE WHOLE EVENT, not just its payload. Correlation used to be
|
|
21
|
+
# merged onto already-emitted events by `Dispatcher#dispatch`, which
|
|
22
|
+
# is what kept an event writable after it had happened; it is set at
|
|
23
|
+
# construction now, because it is part of the transaction and known
|
|
24
|
+
# from `dispatch`'s own argument before anything is emitted.
|
|
25
|
+
#
|
|
26
|
+
# The LOG stays appendable: new events are still recorded. It is
|
|
27
|
+
# each event that stops changing once it exists.
|
|
28
|
+
def emit!
|
|
29
|
+
Freezer.deep(payload)
|
|
30
|
+
Freezer.deep(correlation)
|
|
31
|
+
freeze
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def to_h
|
|
35
|
+
{
|
|
36
|
+
name: name,
|
|
37
|
+
aggregate: aggregate,
|
|
38
|
+
id: id,
|
|
39
|
+
payload: payload,
|
|
40
|
+
occurred_at: occurred_at
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def to_s
|
|
45
|
+
"#{name}(#{aggregate}##{id}) #{payload.inspect}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def inspect = "#<Event #{self}>"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
require_relative "../naming"
|
|
2
|
+
require_relative "value"
|
|
3
|
+
|
|
4
|
+
module Hecks
|
|
5
|
+
module Runtime
|
|
6
|
+
# THE SCALAR AN IDENTITY PATH NAMES.
|
|
7
|
+
#
|
|
8
|
+
# An identity is DECLARED as a path — `identified_by :number` — and
|
|
9
|
+
# this is the one place that reads one. It follows the path and nothing else.
|
|
10
|
+
#
|
|
11
|
+
# What it replaced was `Value.identifier`, which opened a one-field value
|
|
12
|
+
# object and took whatever was inside : that let `identified_by :number` pass
|
|
13
|
+
# for an identity, with the runtime guessing which field had been meant. The
|
|
14
|
+
# guess is gone. A declaration that names no field is now refused when the
|
|
15
|
+
# bluebook loads (`an aggregate that is identified names a field`, `an entity
|
|
16
|
+
# is known by a field`), so by the time anything is dispatched there is
|
|
17
|
+
# always a path here to follow.
|
|
18
|
+
#
|
|
19
|
+
# Usage:
|
|
20
|
+
#
|
|
21
|
+
# Identity.scalar("number.value", account_number_value_object) # => "acct-1"
|
|
22
|
+
#
|
|
23
|
+
module Identity
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
# The head names the ATTRIBUTE and is consumed by whoever looked the value
|
|
27
|
+
# up; what is left is the walk down into it. A path with no fields to walk
|
|
28
|
+
# — an aggregate that declares no identity and falls back to `id` — hands
|
|
29
|
+
# back what it was given, because there is nothing declared to dig for.
|
|
30
|
+
def scalar(path, held)
|
|
31
|
+
_head, *fields = path.to_s.split(".")
|
|
32
|
+
return held if fields.empty?
|
|
33
|
+
|
|
34
|
+
fields.reduce(Value.materialize(held)) do |dug, field|
|
|
35
|
+
dug.is_a?(Hash) ? (dug[field.to_sym] || dug[field]) : nil
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# THE IDENTITY IS THE JOIN OF ITS PARTS, in declaration order. Shared by
|
|
40
|
+
# `CommandInterpreter` (an aggregate acting on itself) and
|
|
41
|
+
# `EntityInterpreter` (a piece addressed through its aggregate) — a piece
|
|
42
|
+
# declares an identity the same shape a head does, so it derives one the
|
|
43
|
+
# same way. `construct` answers `identity_paths` / `identity_heads` /
|
|
44
|
+
# `attribute` (an Aggregate or an Entity, either one) ; `value_owner`
|
|
45
|
+
# answers for coercion (`Value.for_attribute`'s first argument), which for
|
|
46
|
+
# an entity is its OWNING aggregate — an entity's value objects resolve
|
|
47
|
+
# through the aggregate's namespace, not its own.
|
|
48
|
+
#
|
|
49
|
+
# A part the payload does not carry makes the WHOLE identity unresolvable,
|
|
50
|
+
# rather than half of one. Half an identity names nothing, and joining what
|
|
51
|
+
# did arrive would silently name a different record on every dispatch — the
|
|
52
|
+
# precise failure that minting an id caused, arrived at by another road.
|
|
53
|
+
def of(construct, args, value_owner: construct)
|
|
54
|
+
paths = construct.identity_paths
|
|
55
|
+
return nil if paths.empty?
|
|
56
|
+
|
|
57
|
+
parts = paths.map { |path| from(construct, args, path, value_owner: value_owner) }
|
|
58
|
+
# A BLANK PART NAMES NOTHING, the same as an ABSENT one — AN ID IS A
|
|
59
|
+
# SCALAR, and "" is not a fact about anything. This used to check only
|
|
60
|
+
# `nil?`, so a canonical text extracted as "" (an expression whose
|
|
61
|
+
# source did not survive extraction) resolved to a REAL, empty-string
|
|
62
|
+
# identity — a record addressable by an id no caller could have meant.
|
|
63
|
+
return nil if parts.any? { |part| part.nil? || (part.respond_to?(:empty?) && part.empty?) }
|
|
64
|
+
|
|
65
|
+
Naming.identity(parts)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# A path digs into the value object that carries the identity, so what is
|
|
69
|
+
# stored is the SCALAR inside it rather than the object serialised whole.
|
|
70
|
+
def from(construct, args, key, value_owner: construct)
|
|
71
|
+
return nil unless key
|
|
72
|
+
|
|
73
|
+
head, *rest = key.to_s.split(".")
|
|
74
|
+
head = head.to_sym
|
|
75
|
+
return nil unless args.key?(head)
|
|
76
|
+
|
|
77
|
+
unless rest.empty?
|
|
78
|
+
held = args[head]
|
|
79
|
+
held = held.to_h if held.respond_to?(:to_h)
|
|
80
|
+
# AN ID IS ALWAYS A SCALAR. The path says WHICH FIELD carries it, so a
|
|
81
|
+
# caller may hand that field's value straight over — a string or a
|
|
82
|
+
# number, never a serialised object. Only a value object that actually
|
|
83
|
+
# arrived whole has to be opened.
|
|
84
|
+
return held.to_s unless held.is_a?(Hash)
|
|
85
|
+
|
|
86
|
+
return rest.reduce(held) { |h, f| h.is_a?(Hash) ? (h[f.to_sym] || h[f]) : nil }&.to_s
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Coerced against the identity ATTRIBUTE only when the caller actually
|
|
90
|
+
# named it. A saga addresses an aggregate by its correlation key, and
|
|
91
|
+
# that key carries the id ALREADY RESOLVED — coercing "w1" against a
|
|
92
|
+
# WireReference asked the caller to pass fields for a value object they
|
|
93
|
+
# never mentioned.
|
|
94
|
+
attribute = construct.identity_heads.include?(head) ? construct.attribute(head) : nil
|
|
95
|
+
raw = args[head]
|
|
96
|
+
return raw unless attribute
|
|
97
|
+
|
|
98
|
+
# AN ID IS ALWAYS A SCALAR — same contract the dotted branch above
|
|
99
|
+
# already keeps, just reached a different way here: a BARE
|
|
100
|
+
# (undotted) identity path names one of THIS construct's own
|
|
101
|
+
# declared attributes directly, and when that attribute's type is
|
|
102
|
+
# a value object (Translation's own compound `identified_by
|
|
103
|
+
# :domain, :from, :to`, each typed `TranslationDomainName`/
|
|
104
|
+
# `TranslationEraName`), `Value.for_attribute` coerces it into a
|
|
105
|
+
# real single-field Value wrapper — never unwrapped before this,
|
|
106
|
+
# so `Naming.identity`'s own plain `Array#join` (`Naming.identity`'s
|
|
107
|
+
# own header: parts must already be scalars) fell through to
|
|
108
|
+
# Ruby's default `Object#to_s`, leaking a raw, run-to-run-random
|
|
109
|
+
# memory address (`#<Hecks::Runtime::Value:0x...>`) into
|
|
110
|
+
# every refusal quoting this identity — found live via bin/fuzz on
|
|
111
|
+
# the self-hosted "translation" domain (replay_is_deterministic:
|
|
112
|
+
# the SAME address never repeats, so two replays of the
|
|
113
|
+
# identical steps produced different histories the moment a
|
|
114
|
+
# Translation went missing). `materialize_unwrapped` is the
|
|
115
|
+
# SAME single-field-VO-recurses-to-its-bare-scalar helper
|
|
116
|
+
# `read_model_interpreter.rb` already uses for exactly this
|
|
117
|
+
# unwrap; passthrough for anything that isn't a Value at all.
|
|
118
|
+
Value.materialize_unwrapped(Value.for_attribute(value_owner, attribute, raw)).to_s
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# How an identity READS when the runtime has to name it in a refusal — the
|
|
122
|
+
# paths as they were declared, so the message quotes the bluebook back.
|
|
123
|
+
def reading(construct)
|
|
124
|
+
construct.identity_paths.join(", ")
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
require_relative "value"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Runtime
|
|
5
|
+
class Instance
|
|
6
|
+
attr_reader :aggregate, :id
|
|
7
|
+
attr_accessor :state
|
|
8
|
+
|
|
9
|
+
def initialize(aggregate:, id:, state: nil)
|
|
10
|
+
@aggregate = aggregate
|
|
11
|
+
@id = id
|
|
12
|
+
@state = state ? self.class.hydrate_with_defaults(aggregate, state) : self.class.defaults(aggregate)
|
|
13
|
+
materialize_identity!
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Loading existing state runs the same default-fill a fresh instance
|
|
17
|
+
# gets: an attribute the record predates — a newly-required field
|
|
18
|
+
# with a declared default:, a list added since the record was
|
|
19
|
+
# written — arrives filled instead of nil. Only declared defaults
|
|
20
|
+
# fill in; an attribute with no default stays absent, exactly as
|
|
21
|
+
# stored.
|
|
22
|
+
def self.hydrate_with_defaults(aggregate, state)
|
|
23
|
+
hydrated = Value.hydrate(aggregate, state)
|
|
24
|
+
defaults(aggregate).each do |name, value|
|
|
25
|
+
hydrated[name] = value unless value.nil? || hydrated.key?(name)
|
|
26
|
+
end
|
|
27
|
+
hydrated
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.defaults(aggregate)
|
|
31
|
+
state = aggregate.attributes.each_with_object({}) do |attr, acc|
|
|
32
|
+
# FROZEN, like a list that has had something appended to it.
|
|
33
|
+
# An untouched list is the easiest one to miss and the easiest
|
|
34
|
+
# to mutate: nothing has replaced it yet, so a caller pushing
|
|
35
|
+
# into it writes straight into the aggregate's own state.
|
|
36
|
+
acc[attr.name] = attr.list? ? Freezer.deep([]) : default_for(aggregate, attr)
|
|
37
|
+
end
|
|
38
|
+
state[aggregate.lifecycle.field.to_sym] = aggregate.lifecycle.default if aggregate.lifecycle
|
|
39
|
+
state
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.default_for(aggregate, attribute)
|
|
43
|
+
return Value.for_attribute(aggregate, attribute, attribute.default) unless attribute.default.nil?
|
|
44
|
+
# An entity's members hydrate through the same path but an entity
|
|
45
|
+
# declares no value objects of its own — nothing to default-build.
|
|
46
|
+
return nil unless aggregate.respond_to?(:value_object)
|
|
47
|
+
|
|
48
|
+
value_object = aggregate.value_object(attribute.type)
|
|
49
|
+
return nil unless value_object && value_object.attributes.all? { |field| !field.default.nil? }
|
|
50
|
+
|
|
51
|
+
Value.build(value_object, {}, aggregate)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def [](name) = @state[name.to_sym]
|
|
55
|
+
|
|
56
|
+
def key?(name) = @state.key?(name.to_sym)
|
|
57
|
+
|
|
58
|
+
def []=(name, value)
|
|
59
|
+
@state[name.to_sym] = value
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def method_missing(name, *args)
|
|
63
|
+
return @state[name] if @state.key?(name)
|
|
64
|
+
|
|
65
|
+
super
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def respond_to_missing?(name, include_private = false)
|
|
69
|
+
@state.key?(name) || super
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# `id: @id` LAST, not first — see Facade::Handle#to_h's own comment
|
|
73
|
+
# for the full story (the same fix, landed there first): an
|
|
74
|
+
# aggregate free to declare its own attribute literally named `id`
|
|
75
|
+
# (BurningManPrep's `Item`, `attribute :id, ItemId`) has that
|
|
76
|
+
# attribute's own wrapped value object sitting in `@state[:id]` —
|
|
77
|
+
# merging `@state` on top of `{ id: @id }` let it silently clobber
|
|
78
|
+
# the correct bare identity. `@id` merged last always wins.
|
|
79
|
+
def to_h = @state.merge(id: @id)
|
|
80
|
+
|
|
81
|
+
# A COPY A MUTATION MAY TOUCH. Every adapter but Memory hands `find`
|
|
82
|
+
# a freshly-decoded Instance already; Memory's holds the record it
|
|
83
|
+
# eventually saves — the SAME state Hash, aliased. Before `ensures`
|
|
84
|
+
# existed, nothing could refuse between apply_mutations and save, so
|
|
85
|
+
# that aliasing was invisible: a dispatch either ran to completion or
|
|
86
|
+
# raised before touching state at all. `ensures` is the first refusal
|
|
87
|
+
# to sit AFTER mutation, and it found the bug the moment it did — an
|
|
88
|
+
# in-memory record left half-mutated by a dispatch that then refused.
|
|
89
|
+
# `command_interpreter`/`entity_interpreter` hydrate an EXISTING
|
|
90
|
+
# record through this, never through the adapter's own return value
|
|
91
|
+
# directly, so a refused ensures leaves the stored record untouched
|
|
92
|
+
# regardless of which adapter is holding it.
|
|
93
|
+
def dup
|
|
94
|
+
copy = super
|
|
95
|
+
copy.state = @state.dup
|
|
96
|
+
copy
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def inspect
|
|
100
|
+
fields = @state.map { |k, v| "#{k}=#{v.inspect}" }.join(" ")
|
|
101
|
+
"#<#{@aggregate.hecks_name} #{@id} #{fields}>"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
def materialize_identity!
|
|
107
|
+
identity = @aggregate.identified_by || :id
|
|
108
|
+
attribute = @aggregate.attribute(identity)
|
|
109
|
+
return unless attribute && @state[identity].nil?
|
|
110
|
+
# Several paths under one head mean the identifier is a display key,
|
|
111
|
+
# not a reversible serialization of the structured value object. A
|
|
112
|
+
# creating command supplies that object explicitly; persisted state
|
|
113
|
+
# hydrates it from storage. Never guess by splitting the joined id.
|
|
114
|
+
return if @aggregate.identity_heads.one? && @aggregate.identity_paths.size > 1
|
|
115
|
+
|
|
116
|
+
@state[identity] = Value.from_identifier(@aggregate, attribute, @id)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|