hecks 1.3.0 → 1.5.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 +4 -4
- data/lib/hecks/adapters/driven/claude_code.rb +72 -7
- data/lib/hecks/adapters/driven/d1.rb +187 -23
- data/lib/hecks/adapters/driven/folder.rb +83 -10
- data/lib/hecks/adapters/driven/google_authentication.rb +33 -12
- data/lib/hecks/adapters/driven/governance_authorization.rb +87 -18
- data/lib/hecks/adapters/driven/heki/journal.rb +13 -4
- data/lib/hecks/adapters/driven/heki/saga_store.rb +56 -10
- data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
- data/lib/hecks/adapters/driven/heki.rb +106 -9
- data/lib/hecks/adapters/driven/identity_registry.rb +12 -2
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +25 -3
- data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
- data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
- data/lib/hecks/adapters/driven/lambda/client.rb +67 -14
- data/lib/hecks/adapters/driven/lambda.rb +82 -35
- data/lib/hecks/adapters/driven/local_storage.rb +83 -10
- data/lib/hecks/adapters/driven/memory.rb +205 -9
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +21 -1
- data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
- data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
- data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
- data/lib/hecks/adapters/driven/postgres.rb +175 -28
- data/lib/hecks/adapters/driven/prism.rb +50 -11
- data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
- data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
- data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
- data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
- data/lib/hecks/adapters/driven/sqlite.rb +181 -21
- data/lib/hecks/adapters/driven/system_clock.rb +3 -0
- data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
- data/lib/hecks/adapters/driven.rb +6 -4
- data/lib/hecks/adapters/driving/github_webhook.rb +31 -18
- data/lib/hecks/behaviors/dsl.rb +60 -2
- data/lib/hecks/behaviors/expectations.rb +190 -29
- data/lib/hecks/behaviors/ir.rb +12 -1
- data/lib/hecks/behaviors/rspec.rb +9 -1
- data/lib/hecks/behaviors/runner.rb +21 -2
- data/lib/hecks/behaviors.rb +9 -1
- data/lib/hecks/bluebook/aggregate.rb +43 -13
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +17 -10
- data/lib/hecks/bluebook/assembly/build.rb +26 -5
- data/lib/hecks/bluebook/assembly/contract.rb +98 -23
- data/lib/hecks/bluebook/assembly/contracts.rb +59 -52
- data/lib/hecks/bluebook/assembly/marks.rb +159 -30
- data/lib/hecks/bluebook/assembly/specializer.rb +38 -21
- data/lib/hecks/bluebook/assembly.rb +32 -14
- data/lib/hecks/bluebook/attribute.rb +26 -12
- data/lib/hecks/bluebook/behaviour/aggregate.rb +43 -11
- data/lib/hecks/bluebook/behaviour/attribute.rb +18 -5
- data/lib/hecks/bluebook/behaviour/chapter.rb +76 -5
- data/lib/hecks/bluebook/behaviour/command.rb +55 -25
- data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -7
- data/lib/hecks/bluebook/behaviour/entity.rb +20 -8
- data/lib/hecks/bluebook/behaviour/hexagon.rb +30 -4
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +27 -6
- data/lib/hecks/bluebook/behaviour/policy.rb +42 -17
- data/lib/hecks/bluebook/behaviour/process_manager.rb +39 -8
- data/lib/hecks/bluebook/behaviour/query.rb +6 -1
- data/lib/hecks/bluebook/behaviour/read_model.rb +29 -8
- data/lib/hecks/bluebook/behaviour/traits.rb +48 -12
- data/lib/hecks/bluebook/behaviour/value_object.rb +21 -9
- data/lib/hecks/bluebook/capabilities.rb +27 -0
- data/lib/hecks/bluebook/chapter.rb +51 -9
- data/lib/hecks/bluebook/command.rb +62 -17
- data/lib/hecks/bluebook/domain_port.rb +34 -9
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +282 -123
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +112 -75
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +81 -2
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +486 -117
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +179 -47
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +284 -122
- data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
- data/lib/hecks/bluebook/dsl/entity_builder.rb +191 -61
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +130 -30
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +38 -21
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
- data/lib/hecks/bluebook/dsl/policy_builder.rb +86 -36
- data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +111 -47
- data/lib/hecks/bluebook/dsl/query_builder.rb +37 -8
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +127 -52
- data/lib/hecks/bluebook/dsl/rule_reference.rb +97 -43
- data/lib/hecks/bluebook/dsl/translation_builder.rb +150 -44
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +68 -20
- data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
- data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
- data/lib/hecks/bluebook/entity.rb +40 -11
- data/lib/hecks/bluebook/expression/ast_json.rb +128 -36
- data/lib/hecks/bluebook/expression/ast_reader.rb +32 -3
- data/lib/hecks/bluebook/expression/canonical_form.rb +55 -16
- data/lib/hecks/bluebook/expression/evaluator.rb +221 -43
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +54 -18
- data/lib/hecks/bluebook/expression/resolver.rb +369 -128
- data/lib/hecks/bluebook/hexagon.rb +35 -1
- data/lib/hecks/bluebook/lifecycle.rb +12 -1
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +2 -1
- data/lib/hecks/bluebook/meta_validator/judge.rb +126 -108
- data/lib/hecks/bluebook/meta_validator/plan.rb +81 -46
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +3 -2
- data/lib/hecks/bluebook/meta_validator/readings.rb +200 -50
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +68 -41
- data/lib/hecks/bluebook/meta_validator/shapes.rb +166 -21
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +286 -46
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +11 -10
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -5
- data/lib/hecks/bluebook/meta_validator.rb +235 -139
- data/lib/hecks/bluebook/model_check.rb +434 -104
- data/lib/hecks/bluebook/pattern_subset.rb +32 -10
- data/lib/hecks/bluebook/policy.rb +15 -13
- data/lib/hecks/bluebook/process_manager.rb +27 -14
- data/lib/hecks/bluebook/project_discovery.rb +5 -0
- data/lib/hecks/bluebook/project_loader.rb +40 -0
- data/lib/hecks/bluebook/project_register.rb +50 -6
- data/lib/hecks/bluebook/query.rb +31 -4
- data/lib/hecks/bluebook/read_model.rb +35 -15
- data/lib/hecks/bluebook/reference.rb +26 -13
- data/lib/hecks/bluebook/smoke_test.rb +46 -23
- data/lib/hecks/bluebook/synthesizer.rb +46 -12
- data/lib/hecks/bluebook/translation.rb +34 -5
- data/lib/hecks/bluebook/value_object.rb +29 -11
- data/lib/hecks/bluebook.rb +5 -6
- data/lib/hecks/codemod.rb +138 -50
- data/lib/hecks/construct.rb +21 -7
- data/lib/hecks/corpus.rb +438 -0
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
- data/lib/hecks/doc/reference.rb +200 -31
- data/lib/hecks/embryonaut_bluebook.rb +38 -15
- data/lib/hecks/facade/cli_door.rb +69 -10
- data/lib/hecks/facade/cli_runner.rb +105 -24
- data/lib/hecks/facade/command_request.rb +23 -0
- data/lib/hecks/facade/handle.rb +155 -35
- data/lib/hecks/facade/json_door.rb +106 -25
- data/lib/hecks/facade/surface/aggregate_door.rb +50 -27
- data/lib/hecks/facade/surface/chapter.rb +26 -17
- data/lib/hecks/facade/surface.rb +16 -3
- data/lib/hecks/facade.rb +15 -4
- data/lib/hecks/forms/app.rb +46 -30
- data/lib/hecks/forms/command_form_renderer.rb +70 -9
- data/lib/hecks/forms/field_renderer.rb +142 -6
- data/lib/hecks/forms/field_shape.rb +183 -19
- data/lib/hecks/forms/html.rb +51 -7
- data/lib/hecks/forms/index_renderer.rb +14 -2
- data/lib/hecks/forms/page.rb +14 -0
- data/lib/hecks/forms/params.rb +120 -23
- data/lib/hecks/forms/port_argument.rb +14 -2
- data/lib/hecks/forms/query_form_renderer.rb +65 -2
- data/lib/hecks/forms/record_renderer.rb +60 -2
- data/lib/hecks/forms/record_table.rb +28 -1
- data/lib/hecks/forms/reference_options.rb +24 -0
- data/lib/hecks/forms/value_object_shape.rb +13 -3
- data/lib/hecks/forms.rb +24 -4
- data/lib/hecks/fqn.rb +59 -1
- data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
- data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
- data/lib/hecks/framework/oidc.json +15 -0
- data/lib/hecks/framework.rb +79 -25
- data/lib/hecks/freezer.rb +27 -11
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +241 -106
- data/lib/hecks/fuzzing/combination_miner.rb +178 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +241 -45
- data/lib/hecks/fuzzing/coverage_campaign.rb +161 -0
- data/lib/hecks/fuzzing/differential.rb +192 -0
- data/lib/hecks/fuzzing/domain_generator.rb +871 -0
- data/lib/hecks/fuzzing/era_boundary.rb +68 -19
- data/lib/hecks/fuzzing/form_census.rb +121 -20
- data/lib/hecks/fuzzing/generated_domain_check.rb +171 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +45 -6
- data/lib/hecks/fuzzing/isolated_boot.rb +137 -80
- data/lib/hecks/fuzzing/nondeterministic.rb +79 -0
- data/lib/hecks/fuzzing/persistence_parity.rb +111 -21
- data/lib/hecks/fuzzing/properties/corrections.rb +34 -9
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +300 -51
- data/lib/hecks/fuzzing/properties/guards.rb +86 -42
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +80 -32
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +40 -35
- data/lib/hecks/fuzzing/properties/outbox.rb +70 -32
- data/lib/hecks/fuzzing/properties/querying.rb +82 -28
- data/lib/hecks/fuzzing/properties.rb +84 -51
- data/lib/hecks/fuzzing/qa_settings.rb +164 -0
- data/lib/hecks/fuzzing/replay.rb +241 -133
- data/lib/hecks/fuzzing/rotation_priority.rb +48 -28
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +139 -0
- data/lib/hecks/fuzzing/self_consistency.rb +315 -137
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +46 -40
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +59 -32
- data/lib/hecks/fuzzing/sequence_generator.rb +158 -23
- data/lib/hecks/fuzzing/shrinker.rb +309 -0
- data/lib/hecks/fuzzing/structural_skips.rb +37 -130
- data/lib/hecks/fuzzing/sweep_depth.rb +12 -4
- data/lib/hecks/fuzzing/target_capabilities.rb +107 -29
- data/lib/hecks/fuzzing/value_generator.rb +110 -22
- data/lib/hecks/fuzzing.rb +5 -0
- data/lib/hecks/grammar/evolve.rb +188 -12
- data/lib/hecks/grammar.rb +53 -7
- data/lib/hecks/ir.rb +51 -20
- data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
- data/lib/hecks/language/bluebook/policy.bluebook +11 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
- data/lib/hecks/language/oidc.json +5 -0
- data/lib/hecks/literal.rb +41 -9
- data/lib/hecks/naming.rb +112 -31
- data/lib/hecks/ports/access_control.rb +53 -2
- data/lib/hecks/ports/agent/answers.rb +83 -6
- data/lib/hecks/ports/agent.rb +119 -35
- data/lib/hecks/ports/authentication.rb +44 -4
- data/lib/hecks/ports/authorization.rb +50 -11
- data/lib/hecks/ports/clock.rb +42 -23
- data/lib/hecks/ports/extraction.rb +16 -0
- data/lib/hecks/ports/identity_assignment.rb +23 -2
- data/lib/hecks/ports/identity_generation.rb +17 -3
- data/lib/hecks/ports/identity_resolution.rb +17 -1
- data/lib/hecks/ports/key_vault.port +6 -0
- data/lib/hecks/ports/key_vault.rb +58 -0
- data/lib/hecks/ports/loading.rb +4 -0
- data/lib/hecks/ports/persistence/append_only.rb +172 -8
- data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
- data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
- data/lib/hecks/ports/persistence/execution.rb +4 -0
- data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
- data/lib/hecks/ports/persistence/plugin.rb +42 -4
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
- data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +34 -5
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +95 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
- data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +48 -8
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +58 -19
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +96 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +9 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
- data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
- data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
- data/lib/hecks/ports/persistence/state_codec.rb +319 -0
- data/lib/hecks/ports/persistence.rb +36 -1
- data/lib/hecks/ports/projection.rb +61 -7
- data/lib/hecks/ports/query/in_memory.rb +41 -8
- data/lib/hecks/ports/query/ordering.rb +21 -6
- data/lib/hecks/ports/query.rb +35 -0
- data/lib/hecks/ports.rb +1 -0
- data/lib/hecks/projections/bootstrap_table.rb +147 -0
- data/lib/hecks/projections/diagrams.rb +315 -79
- data/lib/hecks/projections/glossary/html.rb +91 -3
- data/lib/hecks/projections/glossary/markdown.rb +50 -3
- data/lib/hecks/projections/glossary/mermaid.rb +49 -1
- data/lib/hecks/projections/glossary/sections.rb +1 -1
- data/lib/hecks/projections/glossary/sentences.rb +110 -5
- data/lib/hecks/projections/glossary.rb +173 -31
- data/lib/hecks/projections/ir.rb +1 -1
- data/lib/hecks/projections/model/deviations.rb +62 -17
- data/lib/hecks/projections/model.rb +75 -21
- data/lib/hecks/projections/oidc.rb +45 -8
- data/lib/hecks/projections/parser_table.rb +53 -12
- data/lib/hecks/projections/reference.rb +15 -3
- data/lib/hecks/projections/rust_vocabulary.rb +646 -0
- data/lib/hecks/projections/shape.rb +10 -2
- data/lib/hecks/projections/statements.rb +69 -22
- data/lib/hecks/projections/vocabulary.rb +26 -9
- data/lib/hecks/projections.rb +5 -3
- data/lib/hecks/projector/cli_projector.rb +245 -37
- data/lib/hecks/projector/docs_projector.rb +154 -28
- data/lib/hecks/projector/exporter.rb +104 -29
- data/lib/hecks/projector/ir_projector.rb +7 -1
- data/lib/hecks/projector/narrate_projector.rb +144 -23
- data/lib/hecks/projector/target.rb +42 -18
- data/lib/hecks/projector.rb +86 -18
- data/lib/hecks/query_ir.rb +94 -47
- data/lib/hecks/query_specification/common/comparators.rb +19 -3
- data/lib/hecks/query_specification/common/comparison.rb +109 -27
- data/lib/hecks/query_specification/common/dsl.rb +65 -9
- data/lib/hecks/query_specification/common/null_policy.rb +61 -15
- data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
- data/lib/hecks/query_specification/common/options.rb +25 -0
- data/lib/hecks/query_specification/field_path.rb +69 -15
- data/lib/hecks/query_specification/hop_path.rb +57 -20
- data/lib/hecks/query_specification/read_model/specification.rb +4 -0
- data/lib/hecks/rendering.rb +9 -3
- data/lib/hecks/router/namespace_installer.rb +16 -3
- data/lib/hecks/router.rb +56 -1
- data/lib/hecks/runtime/aggregate_lock.rb +20 -11
- data/lib/hecks/runtime/boot_gates.rb +21 -3
- data/lib/hecks/runtime/caller.rb +40 -8
- data/lib/hecks/runtime/capability_graph.rb +13 -2
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +40 -41
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +45 -67
- data/lib/hecks/runtime/command_interpreter.rb +195 -113
- data/lib/hecks/runtime/command_rules/admissibility.rb +231 -80
- data/lib/hecks/runtime/command_rules/arithmetic.rb +154 -96
- data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
- data/lib/hecks/runtime/command_rules/emission.rb +18 -1
- data/lib/hecks/runtime/command_rules/references.rb +145 -55
- data/lib/hecks/runtime/command_rules.rb +3 -0
- data/lib/hecks/runtime/dependency_planning.rb +56 -11
- data/lib/hecks/runtime/dispatcher.rb +263 -92
- data/lib/hecks/runtime/entity_element.rb +304 -67
- data/lib/hecks/runtime/entity_interpreter.rb +149 -88
- data/lib/hecks/runtime/errors.rb +37 -23
- data/lib/hecks/runtime/event.rb +15 -10
- data/lib/hecks/runtime/identity.rb +91 -23
- data/lib/hecks/runtime/instance.rb +103 -18
- data/lib/hecks/runtime/interpreting.rb +24 -16
- data/lib/hecks/runtime/invocation.rb +358 -0
- data/lib/hecks/runtime/loader.rb +105 -19
- data/lib/hecks/runtime/outbox.rb +164 -26
- data/lib/hecks/runtime/policy_interpreter.rb +73 -60
- data/lib/hecks/runtime/port_operation_interpreter.rb +42 -19
- data/lib/hecks/runtime/query_interpreter.rb +93 -74
- data/lib/hecks/runtime/reaction_invocation.rb +73 -28
- data/lib/hecks/runtime/read_model_interpreter.rb +60 -44
- data/lib/hecks/runtime/rebuild_sweep.rb +32 -4
- data/lib/hecks/runtime/reference_hop.rb +48 -6
- data/lib/hecks/runtime/refusal_wording.rb +142 -115
- data/lib/hecks/runtime/registry/saga_persistence.rb +32 -21
- data/lib/hecks/runtime/registry/verification.rb +153 -28
- data/lib/hecks/runtime/registry.rb +202 -32
- data/lib/hecks/runtime/remote_dispatcher.rb +125 -24
- data/lib/hecks/runtime/routing.rb +36 -154
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +25 -27
- data/lib/hecks/runtime/saga_interpreter.rb +90 -76
- data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
- data/lib/hecks/runtime/tenant_check.rb +33 -13
- data/lib/hecks/runtime/tenant_scope.rb +23 -5
- data/lib/hecks/runtime/value/admission.rb +75 -30
- data/lib/hecks/runtime/value/coercion.rb +555 -142
- data/lib/hecks/runtime/value/entity_list_coercion.rb +132 -60
- data/lib/hecks/runtime/value.rb +71 -21
- data/lib/hecks/runtime.rb +39 -7
- data/lib/hecks/storehouse.rb +368 -72
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
- data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
- data/lib/hecks/version.rb +3 -3
- data/lib/hecks/vocabulary.rb +205 -4
- data/lib/hecks.rb +91 -11
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +19 -11
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +24 -12
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +38 -14
- metadata +28 -2
|
@@ -14,16 +14,26 @@ module Hecks
|
|
|
14
14
|
PORTS = "ports".freeze
|
|
15
15
|
ADAPTERS = "adapters".freeze
|
|
16
16
|
|
|
17
|
+
# @param settings [Hash] accepted but not read by this class
|
|
18
|
+
# @param root [String, nil] accepted but not read by this class
|
|
17
19
|
def initialize(settings: {}, root: nil)
|
|
18
20
|
@settings = settings
|
|
19
21
|
@root = root
|
|
20
22
|
end
|
|
21
23
|
|
|
24
|
+
# Loads the framework's own bundled ports and adapters from `lib/hecks/{ports,adapters}`.
|
|
25
|
+
#
|
|
26
|
+
# @return [void]
|
|
22
27
|
def load_library
|
|
23
28
|
load_each(library(PORTS), %w[*.port])
|
|
24
29
|
load_each(library(ADAPTERS), %w[*/*.adapter */*/*.adapter])
|
|
25
30
|
end
|
|
26
31
|
|
|
32
|
+
# Loads a shared root's own ports and adapters, if one was found.
|
|
33
|
+
#
|
|
34
|
+
# @param root [String, nil] the shared root directory, as `shared_root` resolves it;
|
|
35
|
+
# nil is a no-op
|
|
36
|
+
# @return [void]
|
|
27
37
|
def load_project(root)
|
|
28
38
|
return unless root
|
|
29
39
|
|
|
@@ -31,7 +41,7 @@ module Hecks
|
|
|
31
41
|
load_each(File.join(root, ADAPTERS), %w[*.adapter */*.adapter */*/*.adapter])
|
|
32
42
|
end
|
|
33
43
|
|
|
34
|
-
#
|
|
44
|
+
# Chapters first, judged once, then everything that reads them.
|
|
35
45
|
# A chapter may be split across files (the language's own grammar is
|
|
36
46
|
# nine), and judging file one before files two-through-nine exist
|
|
37
47
|
# refuses references that are perfectly well declared a file later —
|
|
@@ -39,8 +49,9 @@ module Hecks
|
|
|
39
49
|
# already places every `*.bluebook` ahead of hecksagons and worlds,
|
|
40
50
|
# so the window ends at the last chapter pattern rather than at a
|
|
41
51
|
# hand-written list this would otherwise have to keep in step.
|
|
42
|
-
#
|
|
43
|
-
#
|
|
52
|
+
#
|
|
53
|
+
# `environment:` — one more pair of files, loaded last, not a glob.
|
|
54
|
+
# Recovered, not new — see Runtime::Loader.boot's own comment for
|
|
44
55
|
# the provenance. A caller passing `Hecks.boot(path, environment:
|
|
45
56
|
# "production")` gets exactly `environments/production.hecksagon`
|
|
46
57
|
# and `environments/production.world` loaded, whichever exist (a
|
|
@@ -53,11 +64,17 @@ module Hecks
|
|
|
53
64
|
# globs (all non-recursive, none named `environments/*`), so this
|
|
54
65
|
# is the only thing that ever reaches them. Loaded as genuine
|
|
55
66
|
# `Hecks.hecksagon "SameDomain" do ... end` / `Hecks.world
|
|
56
|
-
# "SameDomain" do ... end` blocks —
|
|
67
|
+
# "SameDomain" do ... end` blocks — merged into the base file's
|
|
57
68
|
# own hecksagon/world (Registry#add_hecksagon / #add_world,
|
|
58
69
|
# concatenate/override rather than replace), so an overlay can
|
|
59
70
|
# rebind or add settings for anything the base file declared
|
|
60
71
|
# without needing to know what else the base file said.
|
|
72
|
+
#
|
|
73
|
+
# @param directory [String] the domain's bluebook directory to load, as
|
|
74
|
+
# `bluebook_directory` resolves it
|
|
75
|
+
# @param environment [String, nil] the environment overlay to load after the domain's
|
|
76
|
+
# own files (e.g. `"production"`); nil loads no overlay
|
|
77
|
+
# @return [void]
|
|
61
78
|
def load_domain(directory, environment: nil)
|
|
62
79
|
boundary = DOMAIN_ORDER.rindex { |pattern| pattern.end_with?(".bluebook") }
|
|
63
80
|
if boundary
|
|
@@ -73,18 +90,23 @@ module Hecks
|
|
|
73
90
|
load_each(directory, [File.join("environments", "#{environment}.world")])
|
|
74
91
|
end
|
|
75
92
|
|
|
76
|
-
#
|
|
93
|
+
# Every bluebook in a folder is one declaration set. Individual files
|
|
77
94
|
# remain organized in the domain expert's language; the folder is the
|
|
78
95
|
# unit callers load. Builders group declarations by the chapter name in
|
|
79
96
|
# each file, so a folder may hold more than one chapter without a catalog.
|
|
80
97
|
# Sorting makes source order deterministic while the deferred window keeps
|
|
81
98
|
# cross-file references from being judged against a partial chapter.
|
|
99
|
+
#
|
|
100
|
+
# @param directory [String] the directory to glob for bluebook chapter files
|
|
101
|
+
# @param patterns [Array<String>] glob patterns, relative to `directory`, selecting the
|
|
102
|
+
# chapter files to load
|
|
103
|
+
# @return [void]
|
|
82
104
|
def load_bluebooks(directory, patterns = ["*.bluebook"])
|
|
83
105
|
Bluebook::MetaValidator.defer { load_each(directory, patterns) }
|
|
84
106
|
Bluebook::MetaValidator.judge_deferred!(Hecks.current_registry)
|
|
85
107
|
end
|
|
86
108
|
|
|
87
|
-
#
|
|
109
|
+
# The explicit-file sibling of `load_domain` — for a caller that names
|
|
88
110
|
# its own exact files rather than a directory to glob (`Loader.boot_files`,
|
|
89
111
|
# behind `Hecks.boot_files`). No `Dir.glob`, no copying: every path here
|
|
90
112
|
# is a real file on disk, wherever it actually lives, loaded in place —
|
|
@@ -93,7 +115,7 @@ module Hecks
|
|
|
93
115
|
# tmpdir (see Loader.boot_files's own header for why that pattern is a
|
|
94
116
|
# hazard, not a convenience).
|
|
95
117
|
#
|
|
96
|
-
#
|
|
118
|
+
# Ordered by category, not by the caller's own list order — same four
|
|
97
119
|
# groups `Vocabulary.fetch("LoadOrder")` walks a directory in
|
|
98
120
|
# (bluebook chapters, translations, hecksagons, worlds), because a
|
|
99
121
|
# hecksagon can reference a bluebook's own constants and must not load
|
|
@@ -101,6 +123,10 @@ module Hecks
|
|
|
101
123
|
# "x.hecksagon", "x.bluebook"` in. Bluebook chapters are judged as one
|
|
102
124
|
# deferred group exactly like `load_domain` does, for the identical
|
|
103
125
|
# forward-reference reason (MetaValidator.defer's own header).
|
|
126
|
+
# @param files [Array<String>] file paths to load, relative to `bluebook_directory`
|
|
127
|
+
# @param environment [String, nil] the environment name whose overlay, if present,
|
|
128
|
+
# loads after the selected files
|
|
129
|
+
# @return [void]
|
|
104
130
|
def load_selected(files, environment: nil)
|
|
105
131
|
bluebooks, rest = files.partition { |f| f.end_with?(".bluebook") }
|
|
106
132
|
|
|
@@ -120,6 +146,14 @@ module Hecks
|
|
|
120
146
|
load_each(directory, [File.join("environments", "#{environment}.world")])
|
|
121
147
|
end
|
|
122
148
|
|
|
149
|
+
# Loads every file under `directory` matching any of `patterns`, in pattern order.
|
|
150
|
+
#
|
|
151
|
+
# A no-op if `directory` does not exist.
|
|
152
|
+
#
|
|
153
|
+
# @param directory [String] the directory to search
|
|
154
|
+
# @param patterns [Array<String>] glob patterns, relative to `directory`, of files to
|
|
155
|
+
# load with `Kernel.load`
|
|
156
|
+
# @return [void]
|
|
123
157
|
def load_each(directory, patterns)
|
|
124
158
|
return unless File.directory?(directory)
|
|
125
159
|
|
|
@@ -128,6 +162,13 @@ module Hecks
|
|
|
128
162
|
end
|
|
129
163
|
end
|
|
130
164
|
|
|
165
|
+
# Resolves a domain path to the directory that actually holds its bluebook files.
|
|
166
|
+
#
|
|
167
|
+
# @param path [String] a domain directory, holding either a `bluebook/` subdirectory or
|
|
168
|
+
# its chapter files directly
|
|
169
|
+
# @return [String] the `bluebook/` subdirectory's absolute path, if one exists; `path`'s
|
|
170
|
+
# own absolute path otherwise
|
|
171
|
+
# @raise [Errno::ENOENT] if neither directory exists
|
|
131
172
|
def bluebook_directory(path)
|
|
132
173
|
expanded = File.expand_path(path)
|
|
133
174
|
nested = File.join(expanded, "bluebook")
|
|
@@ -138,11 +179,13 @@ module Hecks
|
|
|
138
179
|
raise Errno::ENOENT, "no such domain directory: #{path}"
|
|
139
180
|
end
|
|
140
181
|
|
|
141
|
-
#
|
|
182
|
+
# Finds the nearest domain directory at or above `from`, or nil if there is not one.
|
|
183
|
+
#
|
|
184
|
+
# **The domain you are standing in**. Walks up from `from` — the way git
|
|
142
185
|
# finds `.git` — and answers the nearest directory a boot would accept,
|
|
143
186
|
# or nil if there is not one above you.
|
|
144
187
|
#
|
|
145
|
-
#
|
|
188
|
+
# Marked by a `.hecksagon`, not by a `.bluebook`. Chapters are
|
|
146
189
|
# everywhere: era translations, the language's own self-hosted grammar,
|
|
147
190
|
# and `spec/fixtures`, which holds a dozen unrelated ones in a single
|
|
148
191
|
# directory. A `.hecksagon` is the file that says "this is a domain, and
|
|
@@ -152,13 +195,18 @@ module Hecks
|
|
|
152
195
|
# Both layouts, because `bluebook_directory` above accepts both: a
|
|
153
196
|
# domain directory holding a `bluebook/` subdirectory (every example in
|
|
154
197
|
# this corpus), or one holding the files directly.
|
|
155
|
-
#
|
|
198
|
+
# Normalised to the outer directory. Standing in `examples/banking/bluebook`,
|
|
156
199
|
# the `.hecksagon` is right there, so a plain walk stops on the
|
|
157
200
|
# `bluebook/` directory itself. Both boot identically — `bluebook_directory`
|
|
158
201
|
# accepts either and `Loader.boot` takes `File.dirname` of what it gets,
|
|
159
202
|
# so the registry root comes out the same — but `examples/banking` is the
|
|
160
203
|
# directory a person names, and the one a `.world`'s `dir "data"` reads
|
|
161
204
|
# as relative to.
|
|
205
|
+
#
|
|
206
|
+
# @param from [String] the directory to walk up from; defaults to the process's current
|
|
207
|
+
# working directory
|
|
208
|
+
# @return [String, nil] the resolved domain directory's absolute path, or nil if none is
|
|
209
|
+
# found above `from`
|
|
162
210
|
def domain_root(from = Dir.pwd)
|
|
163
211
|
found = nearest_domain(File.expand_path(from))
|
|
164
212
|
return nil unless found
|
|
@@ -167,6 +215,12 @@ module Hecks
|
|
|
167
215
|
File.basename(found) == "bluebook" && domain?(parent) ? parent : found
|
|
168
216
|
end
|
|
169
217
|
|
|
218
|
+
# Walks up from `current` until it finds a domain directory, or reaches the filesystem
|
|
219
|
+
# root.
|
|
220
|
+
#
|
|
221
|
+
# @param current [String] the absolute directory path to start searching from
|
|
222
|
+
# @return [String, nil] the nearest directory (`current` or an ancestor) that `domain?`
|
|
223
|
+
# accepts, or nil if none is found before the filesystem root
|
|
170
224
|
def nearest_domain(current)
|
|
171
225
|
loop do
|
|
172
226
|
return current if domain?(current)
|
|
@@ -178,11 +232,25 @@ module Hecks
|
|
|
178
232
|
end
|
|
179
233
|
end
|
|
180
234
|
|
|
235
|
+
# Reports whether `directory` is a domain root.
|
|
236
|
+
#
|
|
237
|
+
# @param directory [String] the directory to check
|
|
238
|
+
# @return [Boolean] true if `directory`, or its `bluebook/` subdirectory, holds a
|
|
239
|
+
# `.hecksagon` file
|
|
181
240
|
def domain?(directory)
|
|
182
241
|
!Dir[File.join(directory, "*.hecksagon")].empty? ||
|
|
183
242
|
!Dir[File.join(directory, "bluebook", "*.hecksagon")].empty?
|
|
184
243
|
end
|
|
185
244
|
|
|
245
|
+
# Resolves the shared root a domain's own ports/adapters overlay from.
|
|
246
|
+
#
|
|
247
|
+
# @param given [String, nil] an explicit shared-root override; returned expanded as-is
|
|
248
|
+
# when present
|
|
249
|
+
# @param directory [String] the domain directory to search upward from when `given` is
|
|
250
|
+
# nil
|
|
251
|
+
# @return [String, nil] `given`'s expanded path, or the nearest ancestor of `directory`
|
|
252
|
+
# holding a `ports` or `adapters` folder; nil if none is found before the filesystem
|
|
253
|
+
# root
|
|
186
254
|
def shared_root(given, directory)
|
|
187
255
|
return File.expand_path(given) if given
|
|
188
256
|
|
|
@@ -198,6 +266,11 @@ module Hecks
|
|
|
198
266
|
end
|
|
199
267
|
end
|
|
200
268
|
|
|
269
|
+
# Resolves the framework's own bundled `folder` directory under `lib/hecks`.
|
|
270
|
+
#
|
|
271
|
+
# @param folder [String] `"ports"` or `"adapters"`, the framework's own bundled
|
|
272
|
+
# directory name
|
|
273
|
+
# @return [String] the absolute path to `lib/hecks/<folder>`
|
|
201
274
|
def library(folder)
|
|
202
275
|
File.expand_path("../../#{folder}", __dir__)
|
|
203
276
|
end
|
|
@@ -4,26 +4,26 @@ require_relative "../../ports/authentication"
|
|
|
4
4
|
|
|
5
5
|
module Hecks
|
|
6
6
|
module Adapters
|
|
7
|
-
#
|
|
8
|
-
# implementation today
|
|
9
|
-
# (an embryonaut_console `google_auth.rb`
|
|
10
|
-
# any hecks-based app gets Google sign-in for free
|
|
7
|
+
# **Google's own OIDC handshake** — the `authentication` port's one real
|
|
8
|
+
# implementation today. Consolidates what was hand-rolled per-app
|
|
9
|
+
# (an embryonaut_console `google_auth.rb` did this on its own) into
|
|
10
|
+
# one adapter, so any hecks-based app gets Google sign-in for free, the
|
|
11
11
|
# same "one adapter, reusable everywhere" value every other adapter
|
|
12
|
-
# in this directory already has
|
|
12
|
+
# in this directory already has.
|
|
13
13
|
#
|
|
14
|
-
# `oauth2` does
|
|
14
|
+
# `oauth2` does only the authorization-code exchange (no Rack
|
|
15
15
|
# middleware, no Omniauth strategy indirection) ; `google-id-token`
|
|
16
|
-
# does
|
|
16
|
+
# does only ID-token verification (signature checked against
|
|
17
17
|
# Google's real, rotating JWKS — real, maintained code, never
|
|
18
18
|
# hand-rolled here). Neither library decides what a verified
|
|
19
|
-
# (issuer, subject)
|
|
19
|
+
# (issuer, subject) means — that's `Ports::IdentityResolution`'s
|
|
20
20
|
# job, called by whoever consumes this port's `verify`.
|
|
21
21
|
#
|
|
22
|
-
#
|
|
22
|
+
# Lazy requires, same reasoning `Postgres.connect_for` already
|
|
23
23
|
# holds itself to for `pg`: a domain that never binds
|
|
24
24
|
# `authentication` to this adapter should never need these gems
|
|
25
|
-
# installed. This
|
|
26
|
-
# unconditional require_relative list) ; the
|
|
25
|
+
# installed. This file loads in every boot (driven.rb's own
|
|
26
|
+
# unconditional require_relative list) ; the gems load only where
|
|
27
27
|
# a real handshake actually happens.
|
|
28
28
|
#
|
|
29
29
|
# `GOOGLE_CLIENT_ID`/`GOOGLE_CLIENT_SECRET`/`GOOGLE_REDIRECT_URI` —
|
|
@@ -34,6 +34,10 @@ module Hecks
|
|
|
34
34
|
|
|
35
35
|
module_function
|
|
36
36
|
|
|
37
|
+
# Builds the OAuth2 client for Google's token endpoints.
|
|
38
|
+
#
|
|
39
|
+
# @return [OAuth2::Client] a client configured for Google's OAuth2/OIDC endpoints
|
|
40
|
+
# @raise [KeyError] if `GOOGLE_CLIENT_ID` or `GOOGLE_CLIENT_SECRET` is unset
|
|
37
41
|
def client
|
|
38
42
|
require "oauth2"
|
|
39
43
|
OAuth2::Client.new(
|
|
@@ -47,6 +51,11 @@ module Hecks
|
|
|
47
51
|
# The URL to send a browser to, carrying a fresh CSRF `state` the
|
|
48
52
|
# caller is responsible for stashing (a session, typically) and
|
|
49
53
|
# checking again in `verify`.
|
|
54
|
+
#
|
|
55
|
+
# @return [Array(String, String)] the URL to send the browser to, and the fresh CSRF
|
|
56
|
+
# `state` embedded in it
|
|
57
|
+
# @raise [KeyError] if `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET` or
|
|
58
|
+
# `GOOGLE_REDIRECT_URI` is unset
|
|
50
59
|
def authorization_url
|
|
51
60
|
state = SecureRandom.hex(24)
|
|
52
61
|
url = client.auth_code.authorize_url(
|
|
@@ -67,8 +76,20 @@ module Hecks
|
|
|
67
76
|
# the raw token, never anything a caller would need to re-verify
|
|
68
77
|
# itself. `email_verified` rides along because a caller granting
|
|
69
78
|
# access off this email needs to know Google actually checked it.
|
|
79
|
+
#
|
|
80
|
+
# @param code [String] the authorization code the provider sent back
|
|
81
|
+
# @param state [String, nil] the state parameter the provider returned; nil is refused
|
|
82
|
+
# @param expected_state [String, nil] the state `authorization_url` handed out before
|
|
83
|
+
# the redirect; nil is refused
|
|
84
|
+
# @return [Hash{Symbol => Object}] `issuer:` and `subject:` (String), `email:` (String,
|
|
85
|
+
# or nil if the token carries none), `email_verified:` (Boolean)
|
|
86
|
+
# @raise [Ports::Authentication::ValidationError] if either state is nil or the two
|
|
87
|
+
# differ, the code exchange fails, the response has no ID token, or the ID token does
|
|
88
|
+
# not verify
|
|
89
|
+
# @raise [KeyError] if `GOOGLE_CLIENT_ID` or `GOOGLE_REDIRECT_URI` is unset, or a
|
|
90
|
+
# verified token lacks an `iss` or `sub` claim
|
|
70
91
|
def verify(code:, state:, expected_state:)
|
|
71
|
-
#
|
|
92
|
+
# **Both gems, before anything else** — not staggered further down
|
|
72
93
|
# this method: the rescue clause below names GoogleIDToken
|
|
73
94
|
# ::ValidationError, and Ruby resolves that constant reference
|
|
74
95
|
# at the moment an exception is being matched, not at parse
|
|
@@ -2,26 +2,26 @@ require "time"
|
|
|
2
2
|
|
|
3
3
|
module Hecks
|
|
4
4
|
module Adapters
|
|
5
|
-
#
|
|
5
|
+
# **The `authorization` port, fulfilled by governance** — same registry,
|
|
6
6
|
# same boot, so "ask Governance" is a dispatch against records
|
|
7
7
|
# already sitting in the store this adapter is handed, not a bridge
|
|
8
8
|
# to a second runtime. `Runtime::Dispatcher.new(registry)` is cheap
|
|
9
9
|
# to build fresh per call (`Registry#capability_graph`'s own
|
|
10
10
|
# neighbor, `#repository`, does the same kind of on-demand build) —
|
|
11
11
|
# nothing here holds one across calls, so there is no boot-order
|
|
12
|
-
# dependency on
|
|
12
|
+
# dependency on when Governance's bluebook loads relative to this
|
|
13
13
|
# adapter, only that it has by the time `holds_role?` is called.
|
|
14
14
|
#
|
|
15
|
-
#
|
|
16
|
-
# answers with every assignment an actor has ever held
|
|
17
|
-
# (`AssignmentsForActor`'s own description
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
15
|
+
# An active assignment, not merely a past one — `RoleAssignment`
|
|
16
|
+
# answers with every assignment an actor has ever held, current or
|
|
17
|
+
# past (`AssignmentsForActor`'s own description covers both), and
|
|
18
|
+
# leaves `ends_at` for the caller to read, the same deferral
|
|
19
|
+
# `Governance::RoleTransition.Allowed` makes for the same reason.
|
|
20
|
+
# This is that caller.
|
|
21
21
|
module GovernanceAuthorization
|
|
22
22
|
module_function
|
|
23
23
|
|
|
24
|
-
# `as_of` and `scope` are
|
|
24
|
+
# `as_of` and `scope` are both optional, same opt-in shape
|
|
25
25
|
# `refuse_role_mismatch` already gives `actor_id` itself — an
|
|
26
26
|
# unbound `as_of` skips the `starts_at` check and an unbound
|
|
27
27
|
# `scope` skips the `scope` check, exactly the behavior before
|
|
@@ -29,9 +29,21 @@ module Hecks
|
|
|
29
29
|
# resolved from `Ports::Clock.now`, called by the caller at the
|
|
30
30
|
# door, never by this adapter — see `Ports::Clock`'s own header
|
|
31
31
|
# for why the dispatch path must not consult the clock itself.
|
|
32
|
+
#
|
|
33
|
+
# @param registry [Runtime::Registry] the booted registry, to resolve the authorization
|
|
34
|
+
# provider's verb
|
|
35
|
+
# @param actor_id [String] the actor whose grants are checked, compared as a String
|
|
36
|
+
# @param role [String, Symbol] the role name to look for, compared as a String
|
|
37
|
+
# @param as_of [Integer, nil] Unix epoch seconds; a grant whose `starts_at` is later, or
|
|
38
|
+
# does not parse as a time, does not count. nil skips the `starts_at` check
|
|
39
|
+
# @param scope [String, nil] the scope the caller acts in; nil skips the scope check
|
|
40
|
+
# @return [Boolean] true if at least one live grant of `role` to `actor_id` passes the
|
|
41
|
+
# `as_of` and `scope` checks
|
|
42
|
+
# @raise [Runtime::WiringError] if the loaded chapters providing `"authorization"` are
|
|
43
|
+
# not exactly one (see `provided_verb`)
|
|
32
44
|
def holds_role?(registry, actor_id:, role:, as_of: nil, scope: nil)
|
|
33
45
|
rows = Runtime::Dispatcher.new(registry).query(
|
|
34
|
-
|
|
46
|
+
provided_verb(registry, :assignments),
|
|
35
47
|
actor_id: { value: actor_id.to_s }
|
|
36
48
|
)
|
|
37
49
|
|
|
@@ -43,22 +55,33 @@ module Hecks
|
|
|
43
55
|
end
|
|
44
56
|
end
|
|
45
57
|
|
|
46
|
-
# `scope`
|
|
58
|
+
# `scope` unchecked when not stated, same as every other opt-in
|
|
47
59
|
# field here — a caller that never says which scope it is acting
|
|
48
60
|
# in gets the pre-scope behavior: any live assignment for the role
|
|
49
61
|
# authorizes, everywhere. A caller that does state one only
|
|
50
|
-
# authorizes against an assignment granted for
|
|
62
|
+
# authorizes against an assignment granted for that scope.
|
|
63
|
+
#
|
|
64
|
+
# @param row [Hash{Symbol => Object}] one `RoleAssignment` row as
|
|
65
|
+
# `Runtime::Dispatcher#query` returns it; `:scope` holds a `{value: String}` Hash
|
|
66
|
+
# @param scope [String, nil] the scope to check against; nil accepts any scope
|
|
67
|
+
# @return [Boolean] true if `scope` is nil, or the row's own scope matches it
|
|
51
68
|
def in_scope?(row, scope)
|
|
52
69
|
scope.nil? || row[:scope][:value] == scope.to_s
|
|
53
70
|
end
|
|
54
71
|
|
|
55
|
-
# `starts_at`
|
|
72
|
+
# `starts_at` is a free-text string in the bluebook (`Timestamp`'s
|
|
56
73
|
# only invariant is "present", not any particular format) — parsed
|
|
57
74
|
# here with `Time.parse` rather than compared lexically, since
|
|
58
75
|
# nothing guarantees every caller writes it zero-padded ISO 8601.
|
|
59
|
-
#
|
|
76
|
+
# Fails closed : a `starts_at` that does not parse is treated as
|
|
60
77
|
# not-yet-started rather than silently ignored, the same direction
|
|
61
78
|
# every other check in this method already fails.
|
|
79
|
+
#
|
|
80
|
+
# @param row [Hash{Symbol => Object}] one `RoleAssignment` row as
|
|
81
|
+
# `Runtime::Dispatcher#query` returns it; `:starts_at` holds a `{value: String}` Hash
|
|
82
|
+
# @param as_of [Integer, nil] Unix epoch seconds to compare against; nil accepts any row
|
|
83
|
+
# @return [Boolean] true if `as_of` is nil, or the row's `starts_at` parses and is at or
|
|
84
|
+
# before `as_of`; false if `starts_at` does not parse as a time
|
|
62
85
|
def started?(row, as_of)
|
|
63
86
|
return true if as_of.nil?
|
|
64
87
|
|
|
@@ -67,35 +90,81 @@ module Hecks
|
|
|
67
90
|
false
|
|
68
91
|
end
|
|
69
92
|
|
|
70
|
-
#
|
|
93
|
+
# Answers whether one role may act as another.
|
|
94
|
+
#
|
|
95
|
+
# **The other half** — may role X act as role Y. `RoleTransition.Allowed`
|
|
71
96
|
# is identified by the exact pair, so at most one row ever comes
|
|
72
97
|
# back ; still read as `.any?` rather than trusting that structurally,
|
|
73
98
|
# the same defensiveness `holds_role?` already has to have anyway
|
|
74
99
|
# since `AssignmentsForActor` can return several.
|
|
100
|
+
#
|
|
101
|
+
# @param registry [Runtime::Registry] the booted registry, to resolve the authorization
|
|
102
|
+
# provider's verb
|
|
103
|
+
# @param from_role [String, Symbol] the role the caller holds, compared as a String
|
|
104
|
+
# @param to_role [String, Symbol] the role the caller wants to act as, compared as a
|
|
105
|
+
# String
|
|
106
|
+
# @return [Boolean] true if a live (not ended) allowance lets `from_role` act as
|
|
107
|
+
# `to_role`
|
|
108
|
+
# @raise [Runtime::WiringError] if the loaded chapters providing `"authorization"` are
|
|
109
|
+
# not exactly one (see `provided_verb`)
|
|
75
110
|
def authorized_as?(registry, from_role:, to_role:)
|
|
76
111
|
rows = Runtime::Dispatcher.new(registry).query(
|
|
77
|
-
|
|
112
|
+
provided_verb(registry, :transitions),
|
|
78
113
|
from_role: { value: from_role.to_s }, to_role: { value: to_role.to_s }
|
|
79
114
|
)
|
|
80
115
|
|
|
81
116
|
rows.any? { |row| row[:ends_at].nil? }
|
|
82
117
|
end
|
|
83
118
|
|
|
84
|
-
#
|
|
119
|
+
# The role itself, not just a yes/no about one — the same
|
|
85
120
|
# `AssignmentsForActor` query `holds_role?` runs, just returning
|
|
86
121
|
# the live (non-revoked) row's `role_name` instead of comparing it
|
|
87
122
|
# against a caller-supplied guess. `nil` for no live assignment at
|
|
88
123
|
# all — the caller's own fallback (an aggregate's own role field,
|
|
89
124
|
# a default) is domain-specific and does not belong here.
|
|
125
|
+
#
|
|
126
|
+
# @param registry [Runtime::Registry] the booted registry, to resolve the authorization
|
|
127
|
+
# provider's verb
|
|
128
|
+
# @param actor_id [String] the actor to look up, compared as a String
|
|
129
|
+
# @return [String, nil] the role name of the actor's first live (not ended) grant, or
|
|
130
|
+
# nil if it has none
|
|
131
|
+
# @raise [Runtime::WiringError] if the loaded chapters providing `"authorization"` are
|
|
132
|
+
# not exactly one (see `provided_verb`)
|
|
90
133
|
def live_role_for(registry, actor_id:)
|
|
91
134
|
rows = Runtime::Dispatcher.new(registry).query(
|
|
92
|
-
|
|
135
|
+
provided_verb(registry, :assignments),
|
|
93
136
|
actor_id: { value: actor_id.to_s }
|
|
94
137
|
)
|
|
95
138
|
|
|
96
139
|
live = rows.find { |row| row[:ends_at].nil? }
|
|
97
140
|
live && live[:role_name][:value]
|
|
98
141
|
end
|
|
142
|
+
|
|
143
|
+
# The verb, read from the provider's own declaration — `provides
|
|
144
|
+
# "authorization", assignments: ..., transitions: ...` on whichever
|
|
145
|
+
# loaded chapter declares it (Governance's, in every boot today).
|
|
146
|
+
# Exactly one provider, the same "the runtime will not choose for
|
|
147
|
+
# you" rule `Ports::Authorization.adapter` applies to adapters.
|
|
148
|
+
#
|
|
149
|
+
# @param registry [Runtime::Registry] the booted registry to search for the
|
|
150
|
+
# `"authorization"` provider
|
|
151
|
+
# @param key [Symbol] which declared verb to read — `:assignments`, `:grant`, or
|
|
152
|
+
# `:transitions`
|
|
153
|
+
# @return [String] the fully-qualified verb the loaded chapter provides for `key`
|
|
154
|
+
# @raise [Runtime::WiringError] if the loaded chapters providing `"authorization"` are
|
|
155
|
+
# not exactly one
|
|
156
|
+
def provided_verb(registry, key)
|
|
157
|
+
providers = registry.authorization_providers
|
|
158
|
+
unless providers.size == 1
|
|
159
|
+
raise Runtime::WiringError,
|
|
160
|
+
"#{providers.size} loaded chapters provide \"authorization\"" \
|
|
161
|
+
"#{" (#{providers.map(&:name).sort.join(', ')})" unless providers.empty?} — " \
|
|
162
|
+
"a role lookup needs exactly one (framework members declaring it: " \
|
|
163
|
+
"#{Framework.providers_of(Bluebook::Capabilities::AUTHORIZATION).join(', ')})"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
providers.first.provided_verb(Bluebook::Capabilities::AUTHORIZATION, key)
|
|
167
|
+
end
|
|
99
168
|
end
|
|
100
169
|
end
|
|
101
170
|
end
|
|
@@ -6,18 +6,23 @@ module Hecks
|
|
|
6
6
|
# The append-only journal beside the snapshot: one JSON line per
|
|
7
7
|
# entry, fsynced on append, replayed over the snapshot on read.
|
|
8
8
|
module Journal
|
|
9
|
+
# Reads the whole journal back in append order, for `AppendOnly#recover!` to replay.
|
|
10
|
+
#
|
|
11
|
+
# @return [Array<Ports::Persistence::Entry>] every journalled entry, state decoded
|
|
12
|
+
# through the state codec; `[]` when the journal file does not exist or is empty
|
|
13
|
+
# @raise [Malformed] if a journal line is not valid JSON
|
|
9
14
|
def entries
|
|
10
15
|
return [] unless File.exist?(@journal_path)
|
|
11
16
|
|
|
12
17
|
File.readlines(@journal_path, chomp: true).reject(&:empty?).map do |line|
|
|
13
18
|
value = JSON.parse(line)
|
|
14
|
-
state = value["state"]
|
|
19
|
+
state = Ports::Persistence::StateCodec.decode(@aggregate, value["state"])
|
|
15
20
|
Ports::Persistence::Entry.new(operation: value.fetch("operation"), id: value.fetch("id"), state: state,
|
|
16
21
|
mirrors: value["mirrors"])
|
|
17
22
|
end
|
|
18
23
|
end
|
|
19
24
|
|
|
20
|
-
# An
|
|
25
|
+
# An explicit, opt-in maintenance operation — never run
|
|
21
26
|
# automatically after an ordinary save/delete. Heki's journal is
|
|
22
27
|
# not a disposable write-ahead log: it is this adapter's own
|
|
23
28
|
# answer to `entries`, and `entries` is a real port contract
|
|
@@ -26,7 +31,7 @@ module Hecks
|
|
|
26
31
|
# and `Registry#projection_current?` to catch a projection up to
|
|
27
32
|
# its authoritative source, and by `bin/history` to show "every
|
|
28
33
|
# journal entry a domain's append-only adapters hold" — the same
|
|
29
|
-
# contract Postgres/Sqlite/D1 uphold by way of a journal
|
|
34
|
+
# contract Postgres/Sqlite/D1 uphold by way of a journal table
|
|
30
35
|
# that is never pruned. A real example (`examples/banking`,
|
|
31
36
|
# `persisted_by("Heki")` + `projected_by("SqliteProjection")`)
|
|
32
37
|
# depends on this today. Compacting throws that full history away
|
|
@@ -46,6 +51,9 @@ module Hecks
|
|
|
46
51
|
# once. A crash before `write` completes leaves the journal
|
|
47
52
|
# fully intact and the prior snapshot untouched, exactly today's
|
|
48
53
|
# existing crash-recovery guarantee.
|
|
54
|
+
#
|
|
55
|
+
# @return [void]
|
|
56
|
+
# @raise [Malformed] if the snapshot or journal file is corrupt
|
|
49
57
|
def compact!
|
|
50
58
|
with_lock do
|
|
51
59
|
current = replay_journal(read_snapshot)
|
|
@@ -95,7 +103,8 @@ module Hecks
|
|
|
95
103
|
end
|
|
96
104
|
|
|
97
105
|
def append_entry(operation, id, state)
|
|
98
|
-
|
|
106
|
+
encoded = Ports::Persistence::StateCodec.encode(@aggregate, state)
|
|
107
|
+
line = "#{JSON.generate(operation: operation, id: id.to_s, state: encoded, mirrors: @entry_mirrors)}\n"
|
|
99
108
|
|
|
100
109
|
# One write, not JSON-then-newline as two: two concurrent
|
|
101
110
|
# appends can only interleave *between* writes, never inside
|
|
@@ -4,31 +4,41 @@ require_relative "journal"
|
|
|
4
4
|
module Hecks
|
|
5
5
|
module Adapters
|
|
6
6
|
class Heki
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
7
|
+
# The optional saga-persistence capability (§2)'s Heki-specific store — a sibling
|
|
8
|
+
# snapshot+journal file pair, keyed by (domain, process manager, correlation).
|
|
9
|
+
#
|
|
10
|
+
# ## Why a sibling file pair, not a table
|
|
11
|
+
#
|
|
12
|
+
# Built the exact same way an aggregate's own persistence already
|
|
13
|
+
# is: `Snapshot`/`Journal` (heki/snapshot.rb, heki/journal.rb)
|
|
14
|
+
# operate generically on `@path`/`@journal_path`/`@entry_mirrors`
|
|
15
|
+
# and never touch `@aggregate`, so this reuses them unchanged
|
|
16
|
+
# rather than re-deriving the same binary framing and
|
|
17
|
+
# crash-recovery replay.
|
|
18
|
+
#
|
|
19
|
+
# ## Where the file lives
|
|
14
20
|
#
|
|
15
21
|
# Reserved file name (`hecks_saga_instances.heki`, matching the
|
|
16
22
|
# `hecks_`-prefix convention every other new saga table in this
|
|
17
23
|
# work uses) avoids colliding with any real aggregate's own
|
|
18
|
-
# `storage_name`. Lives in the
|
|
24
|
+
# `storage_name`. Lives in the same directory an aggregate's own
|
|
19
25
|
# `.heki` file would (`File.dirname(@path)`, `Heki`'s own call
|
|
20
26
|
# below) — which, since Heki's `resolve_path` has no per-domain
|
|
21
|
-
# component at all, is typically shared across
|
|
27
|
+
# component at all, is typically shared across every domain
|
|
22
28
|
# booted from the same `root`. `domain` is therefore carried
|
|
23
29
|
# inside each record and filtered on read, the same reason
|
|
24
30
|
# Postgres's own `hecks_saga_instances` keeps an explicit `domain`
|
|
25
31
|
# column under schema isolation (§3).
|
|
26
32
|
#
|
|
27
|
-
#
|
|
33
|
+
# ## Record shape
|
|
34
|
+
#
|
|
35
|
+
# One flat records hash, keyed by a composite string (Heki's own
|
|
28
36
|
# snapshot format is id-keyed, not tuple-keyed) — never exposed
|
|
29
37
|
# outside this class; `each_saga` yields the five real fields a
|
|
30
38
|
# caller actually wants, not the internal key shape.
|
|
31
39
|
#
|
|
40
|
+
# ## Locking
|
|
41
|
+
#
|
|
32
42
|
# Locked the same way an aggregate's own store is: `with_lock`
|
|
33
43
|
# (`Snapshot`, shared) serializes each save/delete's read-modify-
|
|
34
44
|
# write against `@path`'s own lock file — a saga gets exactly the
|
|
@@ -38,12 +48,27 @@ module Hecks
|
|
|
38
48
|
include Snapshot
|
|
39
49
|
include Journal
|
|
40
50
|
|
|
51
|
+
# @param dir [String] the directory to hold the saga snapshot+journal file pair,
|
|
52
|
+
# shared with the aggregate `.heki` files that already live there
|
|
41
53
|
def initialize(dir)
|
|
42
54
|
@path = File.join(dir, "hecks_saga_instances.heki")
|
|
43
55
|
@journal_path = "#{@path}.journal"
|
|
44
56
|
@entry_mirrors = nil
|
|
45
57
|
end
|
|
46
58
|
|
|
59
|
+
# Upserts one saga instance's checkpoint, keyed by domain, process manager and
|
|
60
|
+
# correlation, under the file lock.
|
|
61
|
+
#
|
|
62
|
+
# @param domain [String] the owning domain, carried in the record and filtered on
|
|
63
|
+
# read
|
|
64
|
+
# @param process_manager [String] the process manager's name
|
|
65
|
+
# @param correlation [String] the instance's correlation value
|
|
66
|
+
# @param state [String] the saga's current state name
|
|
67
|
+
# @param memory [Hash] the saga's working memory to persist
|
|
68
|
+
# @param completed_compensations [Array] the ledger of completed compensable legs;
|
|
69
|
+
# `[]` when none
|
|
70
|
+
# @return [Hash{String => Hash}] the store's full internal records Hash after the
|
|
71
|
+
# write; callers ignore it
|
|
47
72
|
def save_saga(domain, process_manager, correlation, state, memory, completed_compensations = [])
|
|
48
73
|
key = key_for(domain, process_manager, correlation)
|
|
49
74
|
record = { "domain" => domain, "process_manager" => process_manager,
|
|
@@ -59,6 +84,14 @@ module Hecks
|
|
|
59
84
|
end
|
|
60
85
|
end
|
|
61
86
|
|
|
87
|
+
# Removes a finished saga instance's checkpoint, if present, under the file lock; a
|
|
88
|
+
# missing one is not an error.
|
|
89
|
+
#
|
|
90
|
+
# @param domain [String] the owning domain
|
|
91
|
+
# @param process_manager [String] the process manager's name
|
|
92
|
+
# @param correlation [String] the instance's correlation value
|
|
93
|
+
# @return [Hash{String => Hash}] the store's full internal records Hash after the
|
|
94
|
+
# delete; callers ignore it
|
|
62
95
|
def delete_saga(domain, process_manager, correlation)
|
|
63
96
|
key = key_for(domain, process_manager, correlation)
|
|
64
97
|
|
|
@@ -71,6 +104,19 @@ module Hecks
|
|
|
71
104
|
end
|
|
72
105
|
end
|
|
73
106
|
|
|
107
|
+
# Yields every checkpointed saga instance of `domain`, for `Registry
|
|
108
|
+
# #rehydrate_sagas!` to restore at boot.
|
|
109
|
+
#
|
|
110
|
+
# @param domain [String] the owning domain to filter records to
|
|
111
|
+
# @yieldparam process_manager [String] the process manager's name
|
|
112
|
+
# @yieldparam correlation [String] the instance's correlation value
|
|
113
|
+
# @yieldparam state [String] the saga's state name
|
|
114
|
+
# @yieldparam memory [Hash{Symbol => Object}] the saga's memory, Symbol keys at every
|
|
115
|
+
# depth
|
|
116
|
+
# @yieldparam completed_compensations [Array] the completed-compensation ledger, `[]`
|
|
117
|
+
# when the record carries none
|
|
118
|
+
# @return [Enumerator, Hash{String => Hash}] an enumerator over the same five values
|
|
119
|
+
# when no block is given; otherwise the store's full internal records Hash
|
|
74
120
|
def each_saga(domain)
|
|
75
121
|
return enum_for(:each_saga, domain) unless block_given?
|
|
76
122
|
|