hecks 0.2.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/lib/hecks/adapters/driven/claude_code.adapter +3 -0
- data/lib/hecks/adapters/driven/claude_code.rb +127 -0
- data/lib/hecks/adapters/driven/d1.adapter +12 -0
- data/lib/hecks/adapters/driven/d1.rb +427 -0
- data/lib/hecks/adapters/driven/folder.adapter +3 -0
- data/lib/hecks/adapters/driven/folder.rb +199 -0
- data/lib/hecks/adapters/driven/google_authentication.adapter +3 -0
- data/lib/hecks/adapters/driven/google_authentication.rb +100 -0
- data/lib/hecks/adapters/driven/governance_authorization.adapter +3 -0
- data/lib/hecks/adapters/driven/governance_authorization.rb +101 -0
- data/lib/hecks/adapters/driven/heki/journal.rb +56 -0
- data/lib/hecks/adapters/driven/heki/saga_store.rb +99 -0
- data/lib/hecks/adapters/driven/heki/snapshot.rb +65 -0
- data/lib/hecks/adapters/driven/heki.adapter +4 -0
- data/lib/hecks/adapters/driven/heki.rb +179 -0
- data/lib/hecks/adapters/driven/identity_registry.adapter +3 -0
- data/lib/hecks/adapters/driven/identity_registry.rb +26 -0
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +51 -0
- data/lib/hecks/adapters/driven/lambda/client.rb +63 -0
- data/lib/hecks/adapters/driven/lambda.adapter +4 -0
- data/lib/hecks/adapters/driven/lambda.rb +145 -0
- data/lib/hecks/adapters/driven/memory.adapter +3 -0
- data/lib/hecks/adapters/driven/memory.rb +103 -0
- data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +3 -0
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +28 -0
- data/lib/hecks/adapters/driven/postgres/codec.rb +88 -0
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +207 -0
- data/lib/hecks/adapters/driven/postgres.adapter +5 -0
- data/lib/hecks/adapters/driven/postgres.rb +438 -0
- data/lib/hecks/adapters/driven/postgres_era.adapter +17 -0
- data/lib/hecks/adapters/driven/prism.adapter +3 -0
- data/lib/hecks/adapters/driven/prism.rb +80 -0
- data/lib/hecks/adapters/driven/secure_random_identity.adapter +3 -0
- data/lib/hecks/adapters/driven/secure_random_identity.rb +14 -0
- data/lib/hecks/adapters/driven/sql_query_builder.rb +221 -0
- data/lib/hecks/adapters/driven/sqlite/codec.rb +80 -0
- data/lib/hecks/adapters/driven/sqlite/projection.rb +172 -0
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +192 -0
- data/lib/hecks/adapters/driven/sqlite.adapter +9 -0
- data/lib/hecks/adapters/driven/sqlite.rb +306 -0
- data/lib/hecks/adapters/driven/system_clock.adapter +3 -0
- data/lib/hecks/adapters/driven/system_clock.rb +14 -0
- data/lib/hecks/adapters/driven.rb +54 -0
- data/lib/hecks/adapters.rb +6 -0
- data/lib/hecks/behaviors/dsl.rb +125 -0
- data/lib/hecks/behaviors/expectations.rb +340 -0
- data/lib/hecks/behaviors/ir.rb +31 -0
- data/lib/hecks/behaviors/rspec.rb +42 -0
- data/lib/hecks/behaviors/runner.rb +96 -0
- data/lib/hecks/behaviors.rb +25 -0
- data/lib/hecks/bluebook/aggregate.rb +108 -0
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +134 -0
- data/lib/hecks/bluebook/assembly/build.rb +48 -0
- data/lib/hecks/bluebook/assembly/contract.rb +112 -0
- data/lib/hecks/bluebook/assembly/contracts.rb +438 -0
- data/lib/hecks/bluebook/assembly/marks.rb +228 -0
- data/lib/hecks/bluebook/assembly/specializer.rb +70 -0
- data/lib/hecks/bluebook/assembly.rb +91 -0
- data/lib/hecks/bluebook/attribute.rb +96 -0
- data/lib/hecks/bluebook/behaviour/aggregate.rb +83 -0
- data/lib/hecks/bluebook/behaviour/attribute.rb +27 -0
- data/lib/hecks/bluebook/behaviour/chapter.rb +72 -0
- data/lib/hecks/bluebook/behaviour/command.rb +116 -0
- data/lib/hecks/bluebook/behaviour/domain_port.rb +25 -0
- data/lib/hecks/bluebook/behaviour/entity.rb +59 -0
- data/lib/hecks/bluebook/behaviour/hexagon.rb +50 -0
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +68 -0
- data/lib/hecks/bluebook/behaviour/policy.rb +52 -0
- data/lib/hecks/bluebook/behaviour/process_manager.rb +51 -0
- data/lib/hecks/bluebook/behaviour/query.rb +10 -0
- data/lib/hecks/bluebook/behaviour/read_model.rb +29 -0
- data/lib/hecks/bluebook/behaviour/traits.rb +81 -0
- data/lib/hecks/bluebook/behaviour/value_object.rb +33 -0
- data/lib/hecks/bluebook/chapter.rb +78 -0
- data/lib/hecks/bluebook/command.rb +124 -0
- data/lib/hecks/bluebook/domain_port.rb +102 -0
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +34 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +1018 -0
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +348 -0
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +71 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +1087 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +714 -0
- data/lib/hecks/bluebook/dsl/const_shim.rb +81 -0
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +79 -0
- data/lib/hecks/bluebook/dsl/entity_builder.rb +430 -0
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +366 -0
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +163 -0
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +191 -0
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +44 -0
- data/lib/hecks/bluebook/dsl/malformed.rb +7 -0
- data/lib/hecks/bluebook/dsl/policy_builder.rb +135 -0
- data/lib/hecks/bluebook/dsl/port_builder.rb +39 -0
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +142 -0
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +306 -0
- data/lib/hecks/bluebook/dsl/query_builder.rb +113 -0
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +233 -0
- data/lib/hecks/bluebook/dsl/rule_reference.rb +173 -0
- data/lib/hecks/bluebook/dsl/translation_builder.rb +243 -0
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +178 -0
- data/lib/hecks/bluebook/dsl/word_gate.rb +228 -0
- data/lib/hecks/bluebook/dsl/world_builder.rb +117 -0
- data/lib/hecks/bluebook/dsl.rb +45 -0
- data/lib/hecks/bluebook/entity.rb +103 -0
- data/lib/hecks/bluebook/expression/canonical_form.rb +123 -0
- data/lib/hecks/bluebook/expression/evaluator.rb +304 -0
- data/lib/hecks/bluebook/expression/projection.json +218 -0
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +233 -0
- data/lib/hecks/bluebook/expression/resolver.rb +781 -0
- data/lib/hecks/bluebook/expression.rb +13 -0
- data/lib/hecks/bluebook/hexagon.rb +60 -0
- data/lib/hecks/bluebook/lifecycle.rb +42 -0
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +54 -0
- data/lib/hecks/bluebook/meta_validator/judge.rb +621 -0
- data/lib/hecks/bluebook/meta_validator/plan.rb +332 -0
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +51 -0
- data/lib/hecks/bluebook/meta_validator/readings.rb +360 -0
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +351 -0
- data/lib/hecks/bluebook/meta_validator/shapes.rb +266 -0
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +255 -0
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +138 -0
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +78 -0
- data/lib/hecks/bluebook/meta_validator.rb +542 -0
- data/lib/hecks/bluebook/model_check.rb +445 -0
- data/lib/hecks/bluebook/pattern_subset.rb +184 -0
- data/lib/hecks/bluebook/policy.rb +41 -0
- data/lib/hecks/bluebook/process_manager.rb +128 -0
- data/lib/hecks/bluebook/project_discovery.rb +30 -0
- data/lib/hecks/bluebook/project_loader.rb +40 -0
- data/lib/hecks/bluebook/project_register.rb +82 -0
- data/lib/hecks/bluebook/query.rb +61 -0
- data/lib/hecks/bluebook/read_model.rb +109 -0
- data/lib/hecks/bluebook/reference.rb +74 -0
- data/lib/hecks/bluebook/smoke_test.rb +166 -0
- data/lib/hecks/bluebook/synthesizer.rb +95 -0
- data/lib/hecks/bluebook/translation.rb +92 -0
- data/lib/hecks/bluebook/value_object.rb +66 -0
- data/lib/hecks/bluebook.rb +74 -0
- data/lib/hecks/codemod.rb +342 -0
- data/lib/hecks/construct.rb +71 -0
- data/lib/hecks/deploy/bluebook/deploy.bluebook +219 -0
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +4 -0
- data/lib/hecks/deploy/oidc.json +18 -0
- data/lib/hecks/doc/reference.rb +410 -0
- data/lib/hecks/embryonaut_bluebook.rb +75 -0
- data/lib/hecks/facade/cli_door.rb +119 -0
- data/lib/hecks/facade/cli_runner.rb +190 -0
- data/lib/hecks/facade/command_request.rb +105 -0
- data/lib/hecks/facade/handle.rb +173 -0
- data/lib/hecks/facade/json_door.rb +166 -0
- data/lib/hecks/facade/surface/aggregate_door.rb +185 -0
- data/lib/hecks/facade/surface/chapter.rb +107 -0
- data/lib/hecks/facade/surface.rb +48 -0
- data/lib/hecks/facade.rb +44 -0
- data/lib/hecks/forms/app.rb +341 -0
- data/lib/hecks/forms/command_form_renderer.rb +113 -0
- data/lib/hecks/forms/examples/banking_console.bluebook +3 -0
- data/lib/hecks/forms/field_renderer.rb +177 -0
- data/lib/hecks/forms/field_shape.rb +232 -0
- data/lib/hecks/forms/html.rb +84 -0
- data/lib/hecks/forms/index_renderer.rb +35 -0
- data/lib/hecks/forms/page.rb +157 -0
- data/lib/hecks/forms/params.rb +161 -0
- data/lib/hecks/forms/port_argument.rb +46 -0
- data/lib/hecks/forms/query_form_renderer.rb +114 -0
- data/lib/hecks/forms/record_renderer.rb +119 -0
- data/lib/hecks/forms/record_table.rb +68 -0
- data/lib/hecks/forms/reference_options.rb +30 -0
- data/lib/hecks/forms/value_object_shape.rb +46 -0
- data/lib/hecks/forms.rb +54 -0
- data/lib/hecks/fqn.rb +94 -0
- data/lib/hecks/framework/bluebook/compliance.bluebook +1 -0
- data/lib/hecks/framework/bluebook/console_settings.bluebook +489 -0
- data/lib/hecks/framework/bluebook/framework.hecksagon +32 -0
- data/lib/hecks/framework/bluebook/governance.bluebook +145 -0
- data/lib/hecks/framework/bluebook/identity.bluebook +90 -0
- data/lib/hecks/framework/oidc.json +39 -0
- data/lib/hecks/framework.rb +90 -0
- data/lib/hecks/freezer.rb +67 -0
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +527 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +86 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +286 -0
- data/lib/hecks/fuzzing/properties.rb +1192 -0
- data/lib/hecks/fuzzing/replay.rb +668 -0
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +93 -0
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +80 -0
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +96 -0
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +250 -0
- data/lib/hecks/fuzzing/sequence_generator.rb +131 -0
- data/lib/hecks/fuzzing/value_generator.rb +200 -0
- data/lib/hecks/fuzzing.rb +12 -0
- data/lib/hecks/grammar/evolve.rb +324 -0
- data/lib/hecks/grammar/expression.bluebook +420 -0
- data/lib/hecks/grammar/expression_operators.json +1648 -0
- data/lib/hecks/grammar/grammar.hecksagon +20 -0
- data/lib/hecks/grammar/oidc.json +69 -0
- data/lib/hecks/grammar/translation.bluebook +203 -0
- data/lib/hecks/grammar.rb +178 -0
- data/lib/hecks/ir.rb +126 -0
- data/lib/hecks/language/adapter.bluebook +116 -0
- data/lib/hecks/language/bluebook/aggregate.bluebook +581 -0
- data/lib/hecks/language/bluebook/attaches/paging.bluebook +76 -0
- data/lib/hecks/language/bluebook/bluebook.bluebook +256 -0
- data/lib/hecks/language/bluebook/bluebook.hecksagon +24 -0
- data/lib/hecks/language/bluebook/command.bluebook +471 -0
- data/lib/hecks/language/bluebook/entity.bluebook +392 -0
- data/lib/hecks/language/bluebook/policy.bluebook +189 -0
- data/lib/hecks/language/bluebook/process_manager.bluebook +381 -0
- data/lib/hecks/language/bluebook/projection.bluebook +267 -0
- data/lib/hecks/language/bluebook/query.bluebook +245 -0
- data/lib/hecks/language/bluebook/shape.bluebook +292 -0
- data/lib/hecks/language/bluebook/syntax.bluebook +448 -0
- data/lib/hecks/language/bluebook/vocabulary.bluebook +379 -0
- data/lib/hecks/language/hecksagon/adapter_binding.bluebook +51 -0
- data/lib/hecks/language/hecksagon/domain_port.bluebook +76 -0
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +131 -0
- data/lib/hecks/language/hecksagon/port_operation.bluebook +102 -0
- data/lib/hecks/language/oidc.json +333 -0
- data/lib/hecks/language/port.bluebook +120 -0
- data/lib/hecks/language/translation/translation.bluebook +110 -0
- data/lib/hecks/language/translation/translation_aggregate.bluebook +267 -0
- data/lib/hecks/language/world/wiring.bluebook +62 -0
- data/lib/hecks/language/world/world.bluebook +84 -0
- data/lib/hecks/literal.rb +125 -0
- data/lib/hecks/naming.rb +174 -0
- data/lib/hecks/ports/access_control.port +9 -0
- data/lib/hecks/ports/access_control.rb +62 -0
- data/lib/hecks/ports/agent/answers.rb +104 -0
- data/lib/hecks/ports/agent.port +8 -0
- data/lib/hecks/ports/agent.rb +167 -0
- data/lib/hecks/ports/authentication.port +6 -0
- data/lib/hecks/ports/authentication.rb +50 -0
- data/lib/hecks/ports/authorization.port +7 -0
- data/lib/hecks/ports/authorization.rb +62 -0
- data/lib/hecks/ports/clock.port +5 -0
- data/lib/hecks/ports/clock.rb +62 -0
- data/lib/hecks/ports/extraction.port +5 -0
- data/lib/hecks/ports/extraction.rb +37 -0
- data/lib/hecks/ports/identity_assignment.port +5 -0
- data/lib/hecks/ports/identity_assignment.rb +45 -0
- data/lib/hecks/ports/identity_generation.port +5 -0
- data/lib/hecks/ports/identity_generation.rb +49 -0
- data/lib/hecks/ports/identity_resolution.port +5 -0
- data/lib/hecks/ports/identity_resolution.rb +40 -0
- data/lib/hecks/ports/loading.port +4 -0
- data/lib/hecks/ports/loading.rb +13 -0
- data/lib/hecks/ports/persistence/append_only.rb +138 -0
- data/lib/hecks/ports/persistence/binding_policy.rb +56 -0
- data/lib/hecks/ports/persistence/execution.rb +23 -0
- data/lib/hecks/ports/persistence/null_saga_store.rb +25 -0
- data/lib/hecks/ports/persistence/plugin.rb +54 -0
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +169 -0
- data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +128 -0
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +161 -0
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +61 -0
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +304 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +171 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +190 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +478 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +166 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +314 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +168 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +170 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +134 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +137 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +89 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +85 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +43 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +132 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +74 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +758 -0
- data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +120 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +31 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +46 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +102 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +46 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +70 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +72 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +120 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +183 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +41 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +28 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +30 -0
- data/lib/hecks/ports/persistence/plugins/era/translation.rb +11 -0
- data/lib/hecks/ports/persistence/plugins/era.rb +48 -0
- data/lib/hecks/ports/persistence/remote_runtime.rb +42 -0
- data/lib/hecks/ports/persistence/repository_factory.rb +30 -0
- data/lib/hecks/ports/persistence.port +4 -0
- data/lib/hecks/ports/persistence.rb +39 -0
- data/lib/hecks/ports/projection.port +4 -0
- data/lib/hecks/ports/projection.rb +96 -0
- data/lib/hecks/ports/query/in_memory.rb +60 -0
- data/lib/hecks/ports/query/ordering.rb +41 -0
- data/lib/hecks/ports/query.rb +36 -0
- data/lib/hecks/ports.rb +27 -0
- data/lib/hecks/projections/diagrams.rb +642 -0
- data/lib/hecks/projections/ir.rb +18 -0
- data/lib/hecks/projections/model/deviations.rb +98 -0
- data/lib/hecks/projections/model.rb +145 -0
- data/lib/hecks/projections/oidc.rb +110 -0
- data/lib/hecks/projections/parser_table.rb +159 -0
- data/lib/hecks/projections/reference.rb +38 -0
- data/lib/hecks/projections/shape.rb +44 -0
- data/lib/hecks/projections/statements.rb +110 -0
- data/lib/hecks/projections/vocabulary.rb +100 -0
- data/lib/hecks/projections.rb +32 -0
- data/lib/hecks/projector/cli_projector.rb +446 -0
- data/lib/hecks/projector/docs_projector.rb +321 -0
- data/lib/hecks/projector/exporter.rb +158 -0
- data/lib/hecks/projector/ir_projector.rb +18 -0
- data/lib/hecks/projector/narrate_projector.rb +243 -0
- data/lib/hecks/projector/target.rb +97 -0
- data/lib/hecks/projector.rb +186 -0
- data/lib/hecks/query_ir.rb +411 -0
- data/lib/hecks/query_specification/common/authorization_spec.rb +9 -0
- data/lib/hecks/query_specification/common/comparators.rb +25 -0
- data/lib/hecks/query_specification/common/comparison.rb +174 -0
- data/lib/hecks/query_specification/common/cursor_spec.rb +9 -0
- data/lib/hecks/query_specification/common/dsl.rb +58 -0
- data/lib/hecks/query_specification/common/inspection_spec.rb +9 -0
- data/lib/hecks/query_specification/common/limit_spec.rb +9 -0
- data/lib/hecks/query_specification/common/null_policy.rb +92 -0
- data/lib/hecks/query_specification/common/null_semantics.rb +11 -0
- data/lib/hecks/query_specification/common/offset_spec.rb +9 -0
- data/lib/hecks/query_specification/common/options.rb +32 -0
- data/lib/hecks/query_specification/common/order_by.rb +9 -0
- data/lib/hecks/query_specification/common/specification.rb +10 -0
- data/lib/hecks/query_specification/common/where_clause.rb +9 -0
- data/lib/hecks/query_specification/field_path.rb +107 -0
- data/lib/hecks/query_specification/hop_path.rb +132 -0
- data/lib/hecks/query_specification/read_model/specification.rb +18 -0
- data/lib/hecks/query_specification.rb +14 -0
- data/lib/hecks/rendering.rb +48 -0
- data/lib/hecks/router/namespace_installer.rb +157 -0
- data/lib/hecks/router.rb +70 -0
- data/lib/hecks/runtime/aggregate_lock.rb +45 -0
- data/lib/hecks/runtime/boot_gates.rb +41 -0
- data/lib/hecks/runtime/caller.rb +66 -0
- data/lib/hecks/runtime/capability_graph.rb +44 -0
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +135 -0
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +260 -0
- data/lib/hecks/runtime/command_interpreter.rb +497 -0
- data/lib/hecks/runtime/command_rules/admissibility.rb +357 -0
- data/lib/hecks/runtime/command_rules/arithmetic.rb +260 -0
- data/lib/hecks/runtime/command_rules/authorization.rb +65 -0
- data/lib/hecks/runtime/command_rules/emission.rb +34 -0
- data/lib/hecks/runtime/command_rules/references.rb +189 -0
- data/lib/hecks/runtime/command_rules.rb +28 -0
- data/lib/hecks/runtime/dependency_planning.rb +245 -0
- data/lib/hecks/runtime/dispatcher.rb +301 -0
- data/lib/hecks/runtime/entity_element.rb +253 -0
- data/lib/hecks/runtime/entity_interpreter.rb +299 -0
- data/lib/hecks/runtime/errors.rb +122 -0
- data/lib/hecks/runtime/event.rb +51 -0
- data/lib/hecks/runtime/identity.rb +156 -0
- data/lib/hecks/runtime/instance.rb +175 -0
- data/lib/hecks/runtime/interpreting.rb +90 -0
- data/lib/hecks/runtime/loader.rb +184 -0
- data/lib/hecks/runtime/policy_interpreter.rb +366 -0
- data/lib/hecks/runtime/port_operation_interpreter.rb +210 -0
- data/lib/hecks/runtime/query_interpreter.rb +286 -0
- data/lib/hecks/runtime/reaction_invocation.rb +253 -0
- data/lib/hecks/runtime/read_model_interpreter.rb +341 -0
- data/lib/hecks/runtime/rebuild_sweep.rb +74 -0
- data/lib/hecks/runtime/reference_hop.rb +99 -0
- data/lib/hecks/runtime/refusal_wording.rb +126 -0
- data/lib/hecks/runtime/registry/saga_persistence.rb +142 -0
- data/lib/hecks/runtime/registry/verification.rb +246 -0
- data/lib/hecks/runtime/registry.rb +279 -0
- data/lib/hecks/runtime/remote_dispatcher.rb +143 -0
- data/lib/hecks/runtime/routing.rb +96 -0
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +97 -0
- data/lib/hecks/runtime/saga_interpreter.rb +503 -0
- data/lib/hecks/runtime/saga_pending_dispatch.rb +45 -0
- data/lib/hecks/runtime/tenant_check.rb +84 -0
- data/lib/hecks/runtime/tenant_scope.rb +55 -0
- data/lib/hecks/runtime/value/admission.rb +130 -0
- data/lib/hecks/runtime/value/coercion.rb +565 -0
- data/lib/hecks/runtime/value/invariant_violation.rb +5 -0
- data/lib/hecks/runtime/value.rb +125 -0
- data/lib/hecks/runtime.rb +107 -0
- data/lib/hecks/storehouse.rb +632 -0
- data/lib/hecks/version.rb +16 -0
- data/lib/hecks/vocabulary.rb +218 -0
- data/lib/hecks.rb +123 -6
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +90 -0
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +128 -0
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +160 -0
- metadata +412 -222
- data/bin/hecks +0 -7
- data/bin/hecks-package +0 -65
- data/bin/hecks_console +0 -12
- data/bin/hecks_serverless +0 -6
- data/lib/cli/build.rb +0 -14
- data/lib/cli/command_runner.rb +0 -28
- data/lib/cli/console.rb +0 -10
- data/lib/cli/generate.rb +0 -37
- data/lib/cli/hecks-cli.rb +0 -27
- data/lib/cli/test.rb +0 -57
- data/lib/console/commands.rb +0 -8
- data/lib/console/hecks-console.rb +0 -1
- data/lib/packager/README.md +0 -0
- data/lib/packager/app_runner.rb +0 -21
- data/lib/packager/args.rb +0 -26
- data/lib/packager/compatibility/fixnum.rb +0 -6
- data/lib/packager/hecks.rb +0 -39
- data/lib/packager/query_runner.rb +0 -21
- data/lib/packager/resources/Dockerfile +0 -11
- data/lib/packager/resources/app_binary +0 -7
- data/lib/packager/resources/bundle_config +0 -3
- data/lib/packager/resources/traveling-ruby-20150715-2.2.2-linux-x86_64.tar.gz +0 -0
- data/lib/packager/resources/traveling-ruby-20150715-2.2.2-osx.tar.gz +0 -0
- data/lib/serverless/Domain +0 -32
- data/lib/serverless/cli.rb +0 -75
- data/lib/serverless/resources/command_name.js +0 -5
- data/lib/serverless/resources/environment.js +0 -7
- data/lib/serverless/resources/handler.js.tt +0 -28
- data/lib/serverless/resources/run_binary.js +0 -22
- data/lib/serverless/resources/serverless.yml +0 -20
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
require_relative "../../vocabulary"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Adapters
|
|
5
|
+
class Folder
|
|
6
|
+
DOMAIN_ORDER = Hecks::Vocabulary.fetch("LoadOrder")
|
|
7
|
+
PORTS = "ports"
|
|
8
|
+
ADAPTERS = "adapters"
|
|
9
|
+
|
|
10
|
+
def initialize(settings: {}, root: nil)
|
|
11
|
+
@settings = settings
|
|
12
|
+
@root = root
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def load_library
|
|
16
|
+
load_each(library(PORTS), %w[*.port])
|
|
17
|
+
load_each(library(ADAPTERS), %w[*/*.adapter */*/*.adapter])
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def load_project(root)
|
|
21
|
+
return unless root
|
|
22
|
+
|
|
23
|
+
load_each(File.join(root, PORTS), %w[*.port */*.port])
|
|
24
|
+
load_each(File.join(root, ADAPTERS), %w[*.adapter */*.adapter */*/*.adapter])
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# CHAPTERS FIRST, JUDGED ONCE, THEN EVERYTHING THAT READS THEM.
|
|
28
|
+
# A chapter may be split across files (the language's own grammar is
|
|
29
|
+
# nine), and judging file one before files two-through-nine exist
|
|
30
|
+
# refuses references that are perfectly well declared a file later —
|
|
31
|
+
# see MetaValidator.defer for the whole reasoning. DOMAIN_ORDER
|
|
32
|
+
# already places every `*.bluebook` ahead of hecksagons and worlds,
|
|
33
|
+
# so the window ends at the last chapter pattern rather than at a
|
|
34
|
+
# hand-written list this would otherwise have to keep in step.
|
|
35
|
+
# `environment:` — ONE MORE PAIR OF FILES, LOADED LAST, NOT A GLOB.
|
|
36
|
+
# RECOVERED, not new — see Runtime::Loader.boot's own comment for
|
|
37
|
+
# the provenance. A caller passing `Hecks.boot(path, environment:
|
|
38
|
+
# "production")` gets exactly `environments/production.hecksagon`
|
|
39
|
+
# and `environments/production.world` loaded, whichever exist (a
|
|
40
|
+
# missing one is a silent no-op — not every environment overrides
|
|
41
|
+
# both; see docs/implemented/guides/wiring.md's "Swapping wiring per
|
|
42
|
+
# environment" for the motivating case: swapping a driven adapter
|
|
43
|
+
# — e.g. a real payment gateway for a mock one, or a hosting
|
|
44
|
+
# layer's tenancy settings — without an `if`/`else` anywhere in
|
|
45
|
+
# the domain's own wiring). Never matched by `DOMAIN_ORDER`'s own
|
|
46
|
+
# globs (all non-recursive, none named `environments/*`), so this
|
|
47
|
+
# is the only thing that ever reaches them. Loaded as genuine
|
|
48
|
+
# `Hecks.hecksagon "SameDomain" do ... end` / `Hecks.world
|
|
49
|
+
# "SameDomain" do ... end` blocks — MERGED into the base file's
|
|
50
|
+
# own hecksagon/world (Registry#add_hecksagon / #add_world,
|
|
51
|
+
# concatenate/override rather than replace), so an overlay can
|
|
52
|
+
# rebind or add settings for anything the base file declared
|
|
53
|
+
# without needing to know what else the base file said.
|
|
54
|
+
def load_domain(directory, environment: nil)
|
|
55
|
+
boundary = DOMAIN_ORDER.rindex { |pattern| pattern.end_with?(".bluebook") }
|
|
56
|
+
if boundary
|
|
57
|
+
load_bluebooks(directory, DOMAIN_ORDER[0..boundary])
|
|
58
|
+
load_each(directory, DOMAIN_ORDER[(boundary + 1)..])
|
|
59
|
+
else
|
|
60
|
+
load_each(directory, DOMAIN_ORDER)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
return unless environment
|
|
64
|
+
|
|
65
|
+
load_each(directory, [File.join("environments", "#{environment}.hecksagon")])
|
|
66
|
+
load_each(directory, [File.join("environments", "#{environment}.world")])
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# EVERY BLUEBOOK IN A FOLDER IS ONE DECLARATION SET. Individual files
|
|
70
|
+
# remain organized in the domain expert's language; the folder is the
|
|
71
|
+
# unit callers load. Builders group declarations by the chapter name in
|
|
72
|
+
# each file, so a folder may hold more than one chapter without a catalog.
|
|
73
|
+
# Sorting makes source order deterministic while the deferred window keeps
|
|
74
|
+
# cross-file references from being judged against a partial chapter.
|
|
75
|
+
def load_bluebooks(directory, patterns = ["*.bluebook"])
|
|
76
|
+
Bluebook::MetaValidator.defer { load_each(directory, patterns) }
|
|
77
|
+
Bluebook::MetaValidator.judge_deferred!(Hecks.current_registry)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# THE EXPLICIT-FILE SIBLING OF `load_domain` — for a caller that names
|
|
81
|
+
# its own exact files rather than a directory to glob (`Loader.boot_files`,
|
|
82
|
+
# behind `Hecks.boot_files`). No `Dir.glob`, no copying: every path here
|
|
83
|
+
# is a real file on disk, wherever it actually lives, loaded in place —
|
|
84
|
+
# a `.behaviors` file's `loads` scopes a boot this way specifically so a
|
|
85
|
+
# per-test boot never has to fake isolation by staging bluebooks into a
|
|
86
|
+
# tmpdir (see Loader.boot_files's own header for why that pattern is a
|
|
87
|
+
# hazard, not a convenience).
|
|
88
|
+
#
|
|
89
|
+
# ORDERED BY CATEGORY, NOT BY THE CALLER'S OWN LIST ORDER — same four
|
|
90
|
+
# groups `Vocabulary.fetch("LoadOrder")` walks a directory in
|
|
91
|
+
# (bluebook chapters, translations, hecksagons, worlds), because a
|
|
92
|
+
# hecksagon can reference a bluebook's own constants and must not load
|
|
93
|
+
# first regardless of which order a caller happened to write `loads
|
|
94
|
+
# "x.hecksagon", "x.bluebook"` in. Bluebook chapters are judged as one
|
|
95
|
+
# deferred group exactly like `load_domain` does, for the identical
|
|
96
|
+
# forward-reference reason (MetaValidator.defer's own header).
|
|
97
|
+
def load_selected(files, environment: nil)
|
|
98
|
+
bluebooks, rest = files.partition { |f| f.end_with?(".bluebook") }
|
|
99
|
+
|
|
100
|
+
if bluebooks.any?
|
|
101
|
+
Bluebook::MetaValidator.defer { bluebooks.sort.each { |f| Kernel.load(f) } }
|
|
102
|
+
Bluebook::MetaValidator.judge_deferred!(Hecks.current_registry)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
%w[.hecksagon .world].each do |ext|
|
|
106
|
+
rest.select { |f| f.end_with?(ext) }.sort.each { |f| Kernel.load(f) }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
return unless environment
|
|
110
|
+
|
|
111
|
+
directory = File.dirname(files.first)
|
|
112
|
+
load_each(directory, [File.join("environments", "#{environment}.hecksagon")])
|
|
113
|
+
load_each(directory, [File.join("environments", "#{environment}.world")])
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def load_each(directory, patterns)
|
|
117
|
+
return unless File.directory?(directory)
|
|
118
|
+
|
|
119
|
+
patterns.each do |pattern|
|
|
120
|
+
Dir[File.join(directory, pattern)].sort.each { |file| Kernel.load(file) }
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def bluebook_directory(path)
|
|
125
|
+
expanded = File.expand_path(path)
|
|
126
|
+
nested = File.join(expanded, "bluebook")
|
|
127
|
+
|
|
128
|
+
return nested if File.directory?(nested)
|
|
129
|
+
return expanded if File.directory?(expanded)
|
|
130
|
+
|
|
131
|
+
raise Errno::ENOENT, "no such domain directory: #{path}"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# THE DOMAIN YOU ARE STANDING IN. Walks up from `from` — the way git
|
|
135
|
+
# finds `.git` — and answers the nearest directory a boot would accept,
|
|
136
|
+
# or nil if there is not one above you.
|
|
137
|
+
#
|
|
138
|
+
# MARKED BY A `.hecksagon`, NOT BY A `.bluebook`. Chapters are
|
|
139
|
+
# everywhere: era translations, the language's own self-hosted grammar,
|
|
140
|
+
# and `spec/fixtures`, which holds a dozen unrelated ones in a single
|
|
141
|
+
# directory. A `.hecksagon` is the file that says "this is a domain, and
|
|
142
|
+
# here is how its aggregates are stored", which is exactly the claim a
|
|
143
|
+
# caller is relying on when they omit the path.
|
|
144
|
+
#
|
|
145
|
+
# Both layouts, because `bluebook_directory` above accepts both: a
|
|
146
|
+
# domain directory holding a `bluebook/` subdirectory (every example in
|
|
147
|
+
# this corpus), or one holding the files directly.
|
|
148
|
+
# NORMALISED TO THE OUTER DIRECTORY. Standing in `examples/banking/bluebook`,
|
|
149
|
+
# the `.hecksagon` is right there, so a plain walk stops on the
|
|
150
|
+
# `bluebook/` directory itself. Both boot identically — `bluebook_directory`
|
|
151
|
+
# accepts either and `Loader.boot` takes `File.dirname` of what it gets,
|
|
152
|
+
# so the registry root comes out the same — but `examples/banking` is the
|
|
153
|
+
# directory a person names, and the one a `.world`'s `dir "data"` reads
|
|
154
|
+
# as relative to.
|
|
155
|
+
def domain_root(from = Dir.pwd)
|
|
156
|
+
found = nearest_domain(File.expand_path(from))
|
|
157
|
+
return nil unless found
|
|
158
|
+
|
|
159
|
+
parent = File.dirname(found)
|
|
160
|
+
File.basename(found) == "bluebook" && domain?(parent) ? parent : found
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def nearest_domain(current)
|
|
164
|
+
loop do
|
|
165
|
+
return current if domain?(current)
|
|
166
|
+
|
|
167
|
+
parent = File.dirname(current)
|
|
168
|
+
return nil if parent == current
|
|
169
|
+
|
|
170
|
+
current = parent
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def domain?(directory)
|
|
175
|
+
!Dir[File.join(directory, "*.hecksagon")].empty? ||
|
|
176
|
+
!Dir[File.join(directory, "bluebook", "*.hecksagon")].empty?
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def shared_root(given, directory)
|
|
180
|
+
return File.expand_path(given) if given
|
|
181
|
+
|
|
182
|
+
current = directory
|
|
183
|
+
loop do
|
|
184
|
+
return current if File.directory?(File.join(current, PORTS)) ||
|
|
185
|
+
File.directory?(File.join(current, ADAPTERS))
|
|
186
|
+
|
|
187
|
+
parent = File.dirname(current)
|
|
188
|
+
return nil if parent == current
|
|
189
|
+
|
|
190
|
+
current = parent
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def library(folder)
|
|
195
|
+
File.expand_path("../../#{folder}", __dir__)
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
require "securerandom"
|
|
2
|
+
|
|
3
|
+
require_relative "../../ports/authentication"
|
|
4
|
+
|
|
5
|
+
module Hecks
|
|
6
|
+
module Adapters
|
|
7
|
+
# GOOGLE'S OWN OIDC HANDSHAKE — the `authentication` port's one real
|
|
8
|
+
# implementation today, moved here from being hand-rolled per-app
|
|
9
|
+
# (an embryonaut_console `google_auth.rb` used to do exactly this;
|
|
10
|
+
# any hecks-based app gets Google sign-in for free now, the
|
|
11
|
+
# same "one adapter, reusable everywhere" value every other adapter
|
|
12
|
+
# in this directory already has).
|
|
13
|
+
#
|
|
14
|
+
# `oauth2` does ONLY the authorization-code exchange (no Rack
|
|
15
|
+
# middleware, no Omniauth strategy indirection) ; `google-id-token`
|
|
16
|
+
# does ONLY ID-token verification (signature checked against
|
|
17
|
+
# Google's real, rotating JWKS — real, maintained code, never
|
|
18
|
+
# hand-rolled here). Neither library decides what a verified
|
|
19
|
+
# (issuer, subject) MEANS — that's `Ports::IdentityResolution`'s
|
|
20
|
+
# job, called by whoever consumes this port's `verify`.
|
|
21
|
+
#
|
|
22
|
+
# LAZY REQUIRES, same reasoning `Postgres.connect_for` already
|
|
23
|
+
# holds itself to for `pg`: a domain that never binds
|
|
24
|
+
# `authentication` to this adapter should never need these gems
|
|
25
|
+
# installed. This FILE loads in every boot (driven.rb's own
|
|
26
|
+
# unconditional require_relative list) ; the GEMS load only where
|
|
27
|
+
# a real handshake actually happens.
|
|
28
|
+
#
|
|
29
|
+
# `GOOGLE_CLIENT_ID`/`GOOGLE_CLIENT_SECRET`/`GOOGLE_REDIRECT_URI` —
|
|
30
|
+
# no defaults, on purpose: a login mechanism silently
|
|
31
|
+
# half-configured is worse than one that refuses to boot at all.
|
|
32
|
+
module GoogleAuthentication
|
|
33
|
+
ISSUER = "https://accounts.google.com"
|
|
34
|
+
|
|
35
|
+
module_function
|
|
36
|
+
|
|
37
|
+
def client
|
|
38
|
+
require "oauth2"
|
|
39
|
+
OAuth2::Client.new(
|
|
40
|
+
ENV.fetch("GOOGLE_CLIENT_ID"), ENV.fetch("GOOGLE_CLIENT_SECRET"),
|
|
41
|
+
site: "https://oauth2.googleapis.com",
|
|
42
|
+
authorize_url: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
43
|
+
token_url: "/token"
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# The URL to send a browser to, carrying a fresh CSRF `state` the
|
|
48
|
+
# caller is responsible for stashing (a session, typically) and
|
|
49
|
+
# checking again in `verify`.
|
|
50
|
+
def authorization_url
|
|
51
|
+
state = SecureRandom.hex(24)
|
|
52
|
+
url = client.auth_code.authorize_url(
|
|
53
|
+
redirect_uri: ENV.fetch("GOOGLE_REDIRECT_URI"),
|
|
54
|
+
scope: "openid email profile",
|
|
55
|
+
state: state
|
|
56
|
+
)
|
|
57
|
+
[url, state]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Exchanges `code` for tokens, verifies the returned ID token's
|
|
61
|
+
# signature and claims against Google's own JWKS, and confirms
|
|
62
|
+
# `state` matches what `authorization_url` handed out — a
|
|
63
|
+
# mismatch (or any failure in the exchange/verification itself)
|
|
64
|
+
# is a Ports::Authentication::ValidationError, never a
|
|
65
|
+
# silently-empty result. Returns
|
|
66
|
+
# {issuer:, subject:, email:, email_verified:} on success — never
|
|
67
|
+
# the raw token, never anything a caller would need to re-verify
|
|
68
|
+
# itself. `email_verified` rides along because a caller granting
|
|
69
|
+
# access off this email needs to know Google actually checked it.
|
|
70
|
+
def verify(code:, state:, expected_state:)
|
|
71
|
+
# BOTH GEMS, BEFORE ANYTHING ELSE — not staggered further down
|
|
72
|
+
# this method: the rescue clause below names GoogleIDToken
|
|
73
|
+
# ::ValidationError, and Ruby resolves that constant reference
|
|
74
|
+
# at the moment an exception is being matched, not at parse
|
|
75
|
+
# time. An OAuth2::Error raised before a later `require
|
|
76
|
+
# "google-id-token"` line ran would make the rescue clause
|
|
77
|
+
# itself blow up on an undefined constant, masking the real
|
|
78
|
+
# error. Requiring both up front means whichever one raises
|
|
79
|
+
# first, the constant is already there to catch it.
|
|
80
|
+
require "oauth2"
|
|
81
|
+
require "google-id-token"
|
|
82
|
+
|
|
83
|
+
unless state && expected_state && state == expected_state
|
|
84
|
+
raise Ports::Authentication::ValidationError, "state mismatch"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
token = client.auth_code.get_token(code, redirect_uri: ENV.fetch("GOOGLE_REDIRECT_URI"))
|
|
88
|
+
id_token = token.params["id_token"] or raise Ports::Authentication::ValidationError, "no id_token in the response"
|
|
89
|
+
|
|
90
|
+
payload = GoogleIDToken::Validator.new.check(id_token, ENV.fetch("GOOGLE_CLIENT_ID"))
|
|
91
|
+
{
|
|
92
|
+
issuer: payload.fetch("iss"), subject: payload.fetch("sub"),
|
|
93
|
+
email: payload["email"], email_verified: [true, "true"].include?(payload["email_verified"])
|
|
94
|
+
}
|
|
95
|
+
rescue OAuth2::Error, GoogleIDToken::ValidationError => e
|
|
96
|
+
raise Ports::Authentication::ValidationError, e.message
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
require "time"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Adapters
|
|
5
|
+
# THE `authorization` PORT, FULFILLED BY GOVERNANCE — same registry,
|
|
6
|
+
# same boot, so "ask Governance" is a dispatch against records
|
|
7
|
+
# already sitting in the store this adapter is handed, not a bridge
|
|
8
|
+
# to a second runtime. `Runtime::Dispatcher.new(registry)` is cheap
|
|
9
|
+
# to build fresh per call (`Registry#capability_graph`'s own
|
|
10
|
+
# neighbor, `#repository`, does the same kind of on-demand build) —
|
|
11
|
+
# nothing here holds one across calls, so there is no boot-order
|
|
12
|
+
# dependency on WHEN Governance's bluebook loads relative to this
|
|
13
|
+
# adapter, only that it has by the time `holds_role?` is called.
|
|
14
|
+
#
|
|
15
|
+
# AN ACTIVE ASSIGNMENT, not merely a historical one : `RoleAssignment`
|
|
16
|
+
# answers with every assignment an actor has ever held
|
|
17
|
+
# (`AssignmentsForActor`'s own description — "currently or
|
|
18
|
+
# historically") and leaves `ends_at` for the caller to read, the
|
|
19
|
+
# same deferral `Governance::RoleTransition.Allowed` makes for the
|
|
20
|
+
# same reason. This is that caller.
|
|
21
|
+
module GovernanceAuthorization
|
|
22
|
+
module_function
|
|
23
|
+
|
|
24
|
+
# `as_of` and `scope` are BOTH optional, same opt-in shape
|
|
25
|
+
# `refuse_role_mismatch` already gives `actor_id` itself — an
|
|
26
|
+
# unbound `as_of` skips the `starts_at` check and an unbound
|
|
27
|
+
# `scope` skips the `scope` check, exactly the behavior before
|
|
28
|
+
# either existed. Neither is fetched here: `as_of` arrives already
|
|
29
|
+
# resolved from `Ports::Clock.now`, called by the caller at the
|
|
30
|
+
# door, never by this adapter — see `Ports::Clock`'s own header
|
|
31
|
+
# for why the dispatch path must not consult the clock itself.
|
|
32
|
+
def holds_role?(registry, actor_id:, role:, as_of: nil, scope: nil)
|
|
33
|
+
rows = Runtime::Dispatcher.new(registry).query(
|
|
34
|
+
"Governance::RoleAssignment.AssignmentsForActor",
|
|
35
|
+
actor_id: { value: actor_id.to_s }
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
rows.any? do |row|
|
|
39
|
+
row[:role_name][:value] == role.to_s &&
|
|
40
|
+
row[:ends_at].nil? &&
|
|
41
|
+
in_scope?(row, scope) &&
|
|
42
|
+
started?(row, as_of)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# `scope` UNCHECKED WHEN NOT STATED, same as every other opt-in
|
|
47
|
+
# field here — a caller that never says which scope it is acting
|
|
48
|
+
# in gets the pre-scope behavior: any live assignment for the role
|
|
49
|
+
# authorizes, everywhere. A caller that does state one only
|
|
50
|
+
# authorizes against an assignment granted for THAT scope.
|
|
51
|
+
def in_scope?(row, scope)
|
|
52
|
+
scope.nil? || row[:scope][:value] == scope.to_s
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# `starts_at` IS A FREE-TEXT STRING in the bluebook (`Timestamp`'s
|
|
56
|
+
# only invariant is "present", not any particular format) — parsed
|
|
57
|
+
# here with `Time.parse` rather than compared lexically, since
|
|
58
|
+
# nothing guarantees every caller writes it zero-padded ISO 8601.
|
|
59
|
+
# FAILS CLOSED : a `starts_at` that does not parse is treated as
|
|
60
|
+
# not-yet-started rather than silently ignored, the same direction
|
|
61
|
+
# every other check in this method already fails.
|
|
62
|
+
def started?(row, as_of)
|
|
63
|
+
return true if as_of.nil?
|
|
64
|
+
|
|
65
|
+
Time.parse(row[:starts_at][:value].to_s).to_i <= as_of
|
|
66
|
+
rescue ArgumentError, TypeError
|
|
67
|
+
false
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# THE OTHER HALF — may role X act as role Y. `RoleTransition.Allowed`
|
|
71
|
+
# is identified by the exact pair, so at most one row ever comes
|
|
72
|
+
# back ; still read as `.any?` rather than trusting that structurally,
|
|
73
|
+
# the same defensiveness `holds_role?` already has to have anyway
|
|
74
|
+
# since `AssignmentsForActor` can return several.
|
|
75
|
+
def authorized_as?(registry, from_role:, to_role:)
|
|
76
|
+
rows = Runtime::Dispatcher.new(registry).query(
|
|
77
|
+
"Governance::RoleTransition.Allowed",
|
|
78
|
+
from_role: { value: from_role.to_s }, to_role: { value: to_role.to_s }
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
rows.any? { |row| row[:ends_at].nil? }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# THE ROLE ITSELF, not just a yes/no about one — the same
|
|
85
|
+
# `AssignmentsForActor` query `holds_role?` runs, just returning
|
|
86
|
+
# the live (non-revoked) row's `role_name` instead of comparing it
|
|
87
|
+
# against a caller-supplied guess. `nil` for no live assignment at
|
|
88
|
+
# all — the caller's own fallback (an aggregate's own role field,
|
|
89
|
+
# a default) is domain-specific and does not belong here.
|
|
90
|
+
def live_role_for(registry, actor_id:)
|
|
91
|
+
rows = Runtime::Dispatcher.new(registry).query(
|
|
92
|
+
"Governance::RoleAssignment.AssignmentsForActor",
|
|
93
|
+
actor_id: { value: actor_id.to_s }
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
live = rows.find { |row| row[:ends_at].nil? }
|
|
97
|
+
live && live[:role_name][:value]
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
require_relative "../../../ports/persistence/append_only"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Adapters
|
|
5
|
+
class Heki
|
|
6
|
+
# The append-only journal beside the snapshot: one JSON line per
|
|
7
|
+
# entry, fsynced on append, replayed over the snapshot on read.
|
|
8
|
+
module Journal
|
|
9
|
+
def entries
|
|
10
|
+
return [] unless File.exist?(@journal_path)
|
|
11
|
+
|
|
12
|
+
File.readlines(@journal_path, chomp: true).reject(&:empty?).map do |line|
|
|
13
|
+
value = JSON.parse(line)
|
|
14
|
+
state = value["state"]&.transform_keys(&:to_sym)
|
|
15
|
+
Ports::Persistence::Entry.new(operation: value.fetch("operation"), id: value.fetch("id"), state: state,
|
|
16
|
+
mirrors: value["mirrors"])
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def replay_journal(records)
|
|
23
|
+
return records unless File.exist?(@journal_path)
|
|
24
|
+
|
|
25
|
+
File.foreach(@journal_path) do |line|
|
|
26
|
+
entry = JSON.parse(line)
|
|
27
|
+
id = entry.fetch("id")
|
|
28
|
+
|
|
29
|
+
case entry.fetch("operation")
|
|
30
|
+
when "save" then records[id] = entry.fetch("state")
|
|
31
|
+
when "delete" then records.delete(id)
|
|
32
|
+
else raise Malformed, "#{@journal_path}: unknown journal operation #{entry.fetch("operation").inspect}"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
records
|
|
36
|
+
rescue JSON::ParserError => e
|
|
37
|
+
raise Malformed, "#{@journal_path}: json error: #{e.message}"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def append_entry(operation, id, state)
|
|
41
|
+
line = "#{JSON.generate(operation: operation, id: id.to_s, state: state, mirrors: @entry_mirrors)}\n"
|
|
42
|
+
|
|
43
|
+
# One write, not JSON-then-newline as two: two concurrent
|
|
44
|
+
# appends can only interleave *between* writes, never inside
|
|
45
|
+
# one, so this line can't come out split by another process's
|
|
46
|
+
# line landing in the middle of it.
|
|
47
|
+
File.open(@journal_path, "ab") do |journal|
|
|
48
|
+
journal.write(line)
|
|
49
|
+
journal.flush
|
|
50
|
+
journal.fsync
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
require_relative "snapshot"
|
|
2
|
+
require_relative "journal"
|
|
3
|
+
|
|
4
|
+
module Hecks
|
|
5
|
+
module Adapters
|
|
6
|
+
class Heki
|
|
7
|
+
# THE OPTIONAL saga-persistence capability (§2), Heki's own shape —
|
|
8
|
+
# a SIBLING snapshot+journal file pair, built the exact same way an
|
|
9
|
+
# aggregate's own persistence already is: `Snapshot`/`Journal`
|
|
10
|
+
# (heki/snapshot.rb, heki/journal.rb) operate generically on
|
|
11
|
+
# `@path`/`@journal_path`/`@entry_mirrors` and never touch
|
|
12
|
+
# `@aggregate`, so this reuses them unchanged rather than
|
|
13
|
+
# re-deriving the same binary framing and crash-recovery replay.
|
|
14
|
+
#
|
|
15
|
+
# Reserved file name (`hecks_saga_instances.heki`, matching the
|
|
16
|
+
# `hecks_`-prefix convention every other new saga table in this
|
|
17
|
+
# work uses) avoids colliding with any real aggregate's own
|
|
18
|
+
# `storage_name`. Lives in the SAME directory an aggregate's own
|
|
19
|
+
# `.heki` file would (`File.dirname(@path)`, `Heki`'s own call
|
|
20
|
+
# below) — which, since Heki's `resolve_path` has no per-domain
|
|
21
|
+
# component at all, is typically shared across EVERY domain
|
|
22
|
+
# booted from the same `root`. `domain` is therefore carried
|
|
23
|
+
# inside each record and filtered on read, the same reason
|
|
24
|
+
# Postgres's own `hecks_saga_instances` keeps an explicit `domain`
|
|
25
|
+
# column under schema isolation (§3).
|
|
26
|
+
#
|
|
27
|
+
# ONE flat records hash, keyed by a composite string (Heki's own
|
|
28
|
+
# snapshot format is id-keyed, not tuple-keyed) — never exposed
|
|
29
|
+
# outside this class; `each_saga` yields the five real fields a
|
|
30
|
+
# caller actually wants, not the internal key shape.
|
|
31
|
+
#
|
|
32
|
+
# Locked the same way an aggregate's own store is: `with_lock`
|
|
33
|
+
# (`Snapshot`, shared) serializes each save/delete's read-modify-
|
|
34
|
+
# write against `@path`'s own lock file — a saga gets exactly the
|
|
35
|
+
# durability and concurrency-safety this adapter already gives its
|
|
36
|
+
# aggregates, no better, no worse.
|
|
37
|
+
class SagaStore
|
|
38
|
+
include Snapshot
|
|
39
|
+
include Journal
|
|
40
|
+
|
|
41
|
+
def initialize(dir)
|
|
42
|
+
@path = File.join(dir, "hecks_saga_instances.heki")
|
|
43
|
+
@journal_path = "#{@path}.journal"
|
|
44
|
+
@entry_mirrors = nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def save_saga(domain, process_manager, correlation, state, memory, completed_compensations = [])
|
|
48
|
+
key = key_for(domain, process_manager, correlation)
|
|
49
|
+
record = { "domain" => domain, "process_manager" => process_manager,
|
|
50
|
+
"correlation" => correlation, "state" => state, "memory" => memory,
|
|
51
|
+
"completed_compensations" => completed_compensations }
|
|
52
|
+
|
|
53
|
+
with_lock do
|
|
54
|
+
append_entry("save", key, record)
|
|
55
|
+
current = replay_journal(read_snapshot)
|
|
56
|
+
current[key] = record
|
|
57
|
+
write(current)
|
|
58
|
+
@store = current
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def delete_saga(domain, process_manager, correlation)
|
|
63
|
+
key = key_for(domain, process_manager, correlation)
|
|
64
|
+
|
|
65
|
+
with_lock do
|
|
66
|
+
append_entry("delete", key, nil)
|
|
67
|
+
current = replay_journal(read_snapshot)
|
|
68
|
+
current.delete(key)
|
|
69
|
+
write(current)
|
|
70
|
+
@store = current
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def each_saga(domain)
|
|
75
|
+
return enum_for(:each_saga, domain) unless block_given?
|
|
76
|
+
|
|
77
|
+
store.each_value do |record|
|
|
78
|
+
next unless record["domain"] == domain
|
|
79
|
+
|
|
80
|
+
yield record["process_manager"], record["correlation"], record["state"],
|
|
81
|
+
(record["memory"] || {}).transform_keys(&:to_sym),
|
|
82
|
+
record["completed_compensations"] || []
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def store
|
|
89
|
+
@store ||= replay_journal(read_snapshot)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# A plain space-joined key, not anything fancier — a correlation value is real
|
|
93
|
+
# business data (an order id, a customer reference) and nothing
|
|
94
|
+
# here should assume it never contains a space.
|
|
95
|
+
def key_for(domain, process_manager, correlation) = [domain, process_manager, correlation].join(" ")
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
require "zlib"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Adapters
|
|
5
|
+
class Heki
|
|
6
|
+
# The binary snapshot codec: magic-header framing around
|
|
7
|
+
# deflate-compressed, id-sorted JSON. Reading refuses loudly on a
|
|
8
|
+
# short file, a bad magic, or a body neither zlib nor JSON will own.
|
|
9
|
+
module Snapshot
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def read_snapshot
|
|
13
|
+
return {} unless File.exist?(@path)
|
|
14
|
+
|
|
15
|
+
data = File.binread(@path)
|
|
16
|
+
raise Malformed, "#{@path}: too short" if data.bytesize < HEADER_BYTES
|
|
17
|
+
raise Malformed, "#{@path}: bad magic" unless data[0, 4] == MAGIC
|
|
18
|
+
|
|
19
|
+
JSON.parse(Zlib::Inflate.inflate(data[HEADER_BYTES..]))
|
|
20
|
+
rescue Zlib::DataError => e
|
|
21
|
+
raise Malformed, "#{@path}: zlib error: #{e.message}"
|
|
22
|
+
rescue JSON::ParserError => e
|
|
23
|
+
raise Malformed, "#{@path}: json error: #{e.message}"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Temp-file-plus-rename, fsynced before the rename: a reader can
|
|
27
|
+
# only ever see the last complete snapshot or the one before it,
|
|
28
|
+
# never a truncated or partial one — `File.binwrite`'s old
|
|
29
|
+
# truncate-then-write left a window, proportional to the whole
|
|
30
|
+
# dataset, where the file on disk was neither.
|
|
31
|
+
def write(records)
|
|
32
|
+
sorted = records.sort_by { |id, _| id }.to_h
|
|
33
|
+
json = JSON.generate(sorted)
|
|
34
|
+
body = MAGIC + [sorted.size].pack("N") + Zlib::Deflate.deflate(json, Zlib::BEST_COMPRESSION)
|
|
35
|
+
tmp = "#{@path}.tmp.#{Process.pid}.#{object_id}"
|
|
36
|
+
|
|
37
|
+
File.open(tmp, "wb") do |file|
|
|
38
|
+
file.write(body)
|
|
39
|
+
file.flush
|
|
40
|
+
file.fsync
|
|
41
|
+
end
|
|
42
|
+
File.rename(tmp, @path)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Serializes the read-modify-write each save/delete does against
|
|
46
|
+
# the snapshot (and the journal append that precedes it) across
|
|
47
|
+
# processes — `flock` is per-process advisory, so every writer
|
|
48
|
+
# has to go through this to matter, which `with_lock`'s only two
|
|
49
|
+
# callers (`Heki#save`/`#delete`, `SagaStore#save_saga`/
|
|
50
|
+
# `#delete_saga`) both do. The lock file is separate from
|
|
51
|
+
# `@path` so a held lock never blocks `write`'s own rename.
|
|
52
|
+
def with_lock
|
|
53
|
+
File.open(lock_path, File::CREAT | File::RDWR, 0o644) do |lock|
|
|
54
|
+
lock.flock(File::LOCK_EX)
|
|
55
|
+
yield
|
|
56
|
+
ensure
|
|
57
|
+
lock.flock(File::LOCK_UN)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def lock_path = "#{@path}.lock"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|