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,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,62 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Adapters
|
|
3
|
+
# THE `authorization` PORT, FULFILLED BY GOVERNANCE — same registry,
|
|
4
|
+
# same boot, so "ask Governance" is a dispatch against records
|
|
5
|
+
# already sitting in the store this adapter is handed, not a bridge
|
|
6
|
+
# to a second runtime. `Runtime::Dispatcher.new(registry)` is cheap
|
|
7
|
+
# to build fresh per call (`Registry#capability_graph`'s own
|
|
8
|
+
# neighbor, `#repository`, does the same kind of on-demand build) —
|
|
9
|
+
# nothing here holds one across calls, so there is no boot-order
|
|
10
|
+
# dependency on WHEN Governance's bluebook loads relative to this
|
|
11
|
+
# adapter, only that it has by the time `holds_role?` is called.
|
|
12
|
+
#
|
|
13
|
+
# AN ACTIVE ASSIGNMENT, not merely a historical one : `RoleAssignment`
|
|
14
|
+
# answers with every assignment an actor has ever held
|
|
15
|
+
# (`AssignmentsForActor`'s own description — "currently or
|
|
16
|
+
# historically") and leaves `ends_at` for the caller to read, the
|
|
17
|
+
# same deferral `Governance::RoleTransition.Allowed` makes for the
|
|
18
|
+
# same reason. This is that caller.
|
|
19
|
+
module GovernanceAuthorization
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
def holds_role?(registry, actor_id:, role:)
|
|
23
|
+
rows = Runtime::Dispatcher.new(registry).query(
|
|
24
|
+
"Governance::RoleAssignment.AssignmentsForActor",
|
|
25
|
+
actor_id: { value: actor_id.to_s }
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
rows.any? { |row| row[:role_name][:value] == role.to_s && row[:ends_at].nil? }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# THE OTHER HALF — may role X act as role Y. `RoleTransition.Allowed`
|
|
32
|
+
# is identified by the exact pair, so at most one row ever comes
|
|
33
|
+
# back ; still read as `.any?` rather than trusting that structurally,
|
|
34
|
+
# the same defensiveness `holds_role?` already has to have anyway
|
|
35
|
+
# since `AssignmentsForActor` can return several.
|
|
36
|
+
def authorized_as?(registry, from_role:, to_role:)
|
|
37
|
+
rows = Runtime::Dispatcher.new(registry).query(
|
|
38
|
+
"Governance::RoleTransition.Allowed",
|
|
39
|
+
from_role: { value: from_role.to_s }, to_role: { value: to_role.to_s }
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
rows.any? { |row| row[:ends_at].nil? }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# THE ROLE ITSELF, not just a yes/no about one — the same
|
|
46
|
+
# `AssignmentsForActor` query `holds_role?` runs, just returning
|
|
47
|
+
# the live (non-revoked) row's `role_name` instead of comparing it
|
|
48
|
+
# against a caller-supplied guess. `nil` for no live assignment at
|
|
49
|
+
# all — the caller's own fallback (an aggregate's own role field,
|
|
50
|
+
# a default) is domain-specific and does not belong here.
|
|
51
|
+
def live_role_for(registry, actor_id:)
|
|
52
|
+
rows = Runtime::Dispatcher.new(registry).query(
|
|
53
|
+
"Governance::RoleAssignment.AssignmentsForActor",
|
|
54
|
+
actor_id: { value: actor_id.to_s }
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
live = rows.find { |row| row[:ends_at].nil? }
|
|
58
|
+
live && live[:role_name][:value]
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
File.open(@journal_path, "ab") do |journal|
|
|
42
|
+
journal.write(JSON.generate(operation: operation, id: id.to_s, state: state, mirrors: @entry_mirrors))
|
|
43
|
+
journal.write("\n")
|
|
44
|
+
journal.flush
|
|
45
|
+
journal.fsync
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
Binary file
|
|
@@ -0,0 +1,38 @@
|
|
|
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
|
+
def write(records)
|
|
27
|
+
sorted = records.sort_by { |id, _| id }.to_h
|
|
28
|
+
json = JSON.generate(sorted)
|
|
29
|
+
|
|
30
|
+
File.binwrite(
|
|
31
|
+
@path,
|
|
32
|
+
MAGIC + [sorted.size].pack("N") + Zlib::Deflate.deflate(json, Zlib::BEST_COMPRESSION)
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "fileutils"
|
|
3
|
+
require_relative "heki/snapshot"
|
|
4
|
+
require_relative "heki/journal"
|
|
5
|
+
require_relative "heki/saga_store"
|
|
6
|
+
require_relative "../../ports/persistence/append_only"
|
|
7
|
+
require_relative "../../ports/query/in_memory"
|
|
8
|
+
require_relative "in_memory_ordering"
|
|
9
|
+
require_relative "../../runtime/instance"
|
|
10
|
+
|
|
11
|
+
module Hecks
|
|
12
|
+
module Adapters
|
|
13
|
+
# The file-backed store: a compressed snapshot (heki/snapshot.rb)
|
|
14
|
+
# plus an append-only journal beside it (heki/journal.rb). What stays
|
|
15
|
+
# here is the repository surface — find/all/save/delete, and the
|
|
16
|
+
# entry append/project pair the persistence port drives.
|
|
17
|
+
class Heki
|
|
18
|
+
include Snapshot
|
|
19
|
+
include Journal
|
|
20
|
+
|
|
21
|
+
MAGIC = "HEKI"
|
|
22
|
+
HEADER_BYTES = 8
|
|
23
|
+
|
|
24
|
+
class Malformed < StandardError; end
|
|
25
|
+
|
|
26
|
+
attr_reader :aggregate, :path
|
|
27
|
+
|
|
28
|
+
def initialize(aggregate:, settings: {}, root: nil)
|
|
29
|
+
@aggregate = aggregate
|
|
30
|
+
@path = resolve_path(settings, root)
|
|
31
|
+
@journal_path = "#{@path}.journal"
|
|
32
|
+
@events = []
|
|
33
|
+
# THE OPTIONAL saga-persistence capability's own scoping (§2/§4)
|
|
34
|
+
# — falls back to the aggregate's own name for a directly-
|
|
35
|
+
# instantiated adapter (specs), same fallback shape Postgres's
|
|
36
|
+
# own @domain already uses.
|
|
37
|
+
@domain = (settings[:domain] || settings["domain"] || aggregate.name).to_s
|
|
38
|
+
|
|
39
|
+
FileUtils.mkdir_p(File.dirname(@path))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def find(id)
|
|
43
|
+
record = store[id.to_s]
|
|
44
|
+
return nil unless record
|
|
45
|
+
|
|
46
|
+
instance(id.to_s, record)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def all(order_by: nil, direction: :asc)
|
|
50
|
+
records = store.sort_by { |id, _| id }.map { |id, record| instance(id, record) }
|
|
51
|
+
InMemoryOrdering.ordered(records, aggregate: @aggregate, order_by: order_by, direction: direction)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def count = store.size
|
|
55
|
+
|
|
56
|
+
# `registry: context[:registry]` — Memory's own `query` already
|
|
57
|
+
# threads this through; Heki's own never did, which made
|
|
58
|
+
# `none_in_state?` (Ports::Query::InMemory) unconditionally
|
|
59
|
+
# return `true` (its own graceful "no registry, no way to look
|
|
60
|
+
# the target up" default) for EVERY `none_in_state` where-clause
|
|
61
|
+
# against a Heki-backed aggregate — silently excluding nothing,
|
|
62
|
+
# always, no matter the actual target state.
|
|
63
|
+
def query(specification, args = {}, context: {})
|
|
64
|
+
Ports::Query::InMemory.execute(all, specification, args, registry: context[:registry])
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def append(entry)
|
|
68
|
+
@entry_mirrors = entry.mirrors
|
|
69
|
+
append_entry(entry.operation, entry.id, entry.state)
|
|
70
|
+
entry
|
|
71
|
+
ensure
|
|
72
|
+
@entry_mirrors = nil
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def project(entry)
|
|
76
|
+
current = store
|
|
77
|
+
entry.save? ? current[entry.id] = entry.state.dup : current.delete(entry.id)
|
|
78
|
+
write(current)
|
|
79
|
+
@store = current
|
|
80
|
+
entry
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def save(instance)
|
|
84
|
+
entry = Ports::Persistence::Entry.new(operation: "save", id: instance.id.to_s, state: instance.state.dup)
|
|
85
|
+
append(entry)
|
|
86
|
+
project(entry)
|
|
87
|
+
instance
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def delete(id)
|
|
91
|
+
return false unless find(id)
|
|
92
|
+
|
|
93
|
+
entry = Ports::Persistence::Entry.new(operation: "delete", id: id.to_s, state: nil)
|
|
94
|
+
append(entry)
|
|
95
|
+
project(entry)
|
|
96
|
+
true
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def record_event(event) = @events << event
|
|
100
|
+
|
|
101
|
+
def events = @events
|
|
102
|
+
|
|
103
|
+
# ── the OPTIONAL saga-persistence capability (§2) — Heki's own
|
|
104
|
+
# shape (a sibling snapshot+journal file pair, `SagaStore`,
|
|
105
|
+
# heki/saga_store.rb) rather than a table in a store this adapter
|
|
106
|
+
# doesn't have.
|
|
107
|
+
def save_saga(process_manager:, correlation:, state:, memory:)
|
|
108
|
+
saga_store.save_saga(@domain, process_manager.to_s, correlation.to_s, state.to_s, memory)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def delete_saga(process_manager:, correlation:)
|
|
112
|
+
saga_store.delete_saga(@domain, process_manager.to_s, correlation.to_s)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def each_saga(&block) = saga_store.each_saga(@domain, &block)
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
def saga_store
|
|
120
|
+
@saga_store ||= SagaStore.new(File.dirname(@path))
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def instance(id, record)
|
|
124
|
+
Runtime::Instance.new(
|
|
125
|
+
aggregate: @aggregate,
|
|
126
|
+
id: id,
|
|
127
|
+
state: record.transform_keys(&:to_sym)
|
|
128
|
+
)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def store
|
|
132
|
+
@store ||= read
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def read
|
|
136
|
+
snapshot = read_snapshot
|
|
137
|
+
replay_journal(snapshot)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def resolve_path(settings, root)
|
|
141
|
+
declared = settings[:dir] || settings["dir"] || "data"
|
|
142
|
+
dir = declared.start_with?("/") ? declared : File.join(root || Dir.pwd, declared)
|
|
143
|
+
|
|
144
|
+
File.join(dir, "#{@aggregate.storage_name}.heki")
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Adapters
|
|
3
|
+
# THE `identity_resolution` PORT, FULFILLED BY THE Identity FRAMEWORK
|
|
4
|
+
# BLUEBOOK — same registry, same boot, so this is a dispatch against
|
|
5
|
+
# records already sitting in the store this adapter is handed, not a
|
|
6
|
+
# bridge to a second runtime. Same reasoning `GovernanceAuthorization`
|
|
7
|
+
# gives for itself, one port over.
|
|
8
|
+
module IdentityRegistry
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
# `nil` for a pair nothing has linked, the first match otherwise —
|
|
12
|
+
# `ResolvedBy` is a lookup by the exact (issuer, subject) an
|
|
13
|
+
# authenticated token carries, not a listing, so more than one row
|
|
14
|
+
# would mean two links share a pair, which `Link`'s own natural
|
|
15
|
+
# key already prevents by construction.
|
|
16
|
+
def resolve(registry, issuer:, subject:)
|
|
17
|
+
rows = Runtime::Dispatcher.new(registry).query(
|
|
18
|
+
"Identity::ExternalIdentifier.ResolvedBy",
|
|
19
|
+
issuer: { value: issuer.to_s }, subject: { value: subject.to_s }
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
rows.first&.fetch(:identity)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|