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
data/lib/hecks/naming.rb
CHANGED
|
@@ -5,22 +5,32 @@ module Hecks
|
|
|
5
5
|
# two readers never invent two different spellings for the same
|
|
6
6
|
# derivation.
|
|
7
7
|
module Naming
|
|
8
|
-
#
|
|
8
|
+
# What separates the parts of a derived identity.
|
|
9
9
|
#
|
|
10
|
-
# An identity of several parts is their
|
|
10
|
+
# An identity of several parts is their join, and the join has to be spelled the
|
|
11
11
|
# same everywhere or two readers name two different records off one declaration.
|
|
12
12
|
# It was spelled three ways at once here — "::" for an aggregate under a
|
|
13
13
|
# chapter, "." for everything under an aggregate, "#" for the three that keyed
|
|
14
14
|
# off position — and each was written where it happened to be needed. Once the
|
|
15
|
-
# runtime derived the same identity, the runtime made a
|
|
15
|
+
# runtime derived the same identity, the runtime made a fourth, and a reference
|
|
16
16
|
# that resolved by string comparison found nothing.
|
|
17
17
|
IDENTITY_JOIN = ":".freeze
|
|
18
18
|
|
|
19
19
|
module_function
|
|
20
20
|
|
|
21
|
-
#
|
|
21
|
+
# Joins the parts of an identity in declaration order.
|
|
22
|
+
#
|
|
23
|
+
# @param parts [Array<#to_s>, #to_s] one or more identity segments; a bare
|
|
24
|
+
# value is wrapped in a single-element Array
|
|
25
|
+
# @return [String] the segments joined with `IDENTITY_JOIN`
|
|
22
26
|
def identity(parts) = Array(parts).join(IDENTITY_JOIN)
|
|
23
27
|
|
|
28
|
+
# Strips a namespace path down to its last segment.
|
|
29
|
+
#
|
|
30
|
+
# @param type [Module, String, Symbol, #to_s] a `::`-joined constant path, or
|
|
31
|
+
# anything whose `to_s` is one
|
|
32
|
+
# @return [String] the text after the last `::`, or the whole `to_s` if there
|
|
33
|
+
# is none
|
|
24
34
|
def demodulise(type)
|
|
25
35
|
type.to_s.split("::").last.to_s
|
|
26
36
|
end
|
|
@@ -28,10 +38,17 @@ module Hecks
|
|
|
28
38
|
# snake_case -> PascalCase. The name a synthesised closed-set value object
|
|
29
39
|
# takes when an attribute declares one inline. The derivation is part of
|
|
30
40
|
# the IR contract: the same bluebook must always produce the same name.
|
|
41
|
+
#
|
|
42
|
+
# @param text [String, Symbol, #to_s] a snake_case (or already Pascal) name
|
|
43
|
+
# @return [String] the PascalCase form
|
|
31
44
|
def pascal(text)
|
|
32
45
|
text.to_s.split("_").map { |part| part.sub(/\A(.)/) { Regexp.last_match(1).upcase } }.join
|
|
33
46
|
end
|
|
34
47
|
|
|
48
|
+
# PascalCase or camelCase -> snake_case.
|
|
49
|
+
#
|
|
50
|
+
# @param text [String, Symbol, #to_s] a Pascal-, camel-, or already snake-case name
|
|
51
|
+
# @return [String] the lowercase, underscore-separated form
|
|
35
52
|
def snake(text)
|
|
36
53
|
text.to_s
|
|
37
54
|
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
@@ -39,7 +56,7 @@ module Hecks
|
|
|
39
56
|
.downcase
|
|
40
57
|
end
|
|
41
58
|
|
|
42
|
-
#
|
|
59
|
+
# An identifier as a person would say it — `ATMCard` -> "ATM card",
|
|
43
60
|
# `AccrueInterest` -> "Accrue interest", `daily_limit` -> "Daily
|
|
44
61
|
# limit", `Back office` -> "Back office". The same two word-boundary
|
|
45
62
|
# splits `snake` uses, with a space instead of an underscore — plus
|
|
@@ -48,6 +65,9 @@ module Hecks
|
|
|
48
65
|
# nobody says ("Atm"). First word capitalized, the rest lowercased,
|
|
49
66
|
# so a headword reads as sentence case whatever casing it was
|
|
50
67
|
# declared in.
|
|
68
|
+
#
|
|
69
|
+
# @param text [String, Symbol, #to_s] a Pascal-, camel-, or snake-case identifier
|
|
70
|
+
# @return [String] the space-separated, sentence-cased phrase
|
|
51
71
|
def words(text)
|
|
52
72
|
parts = text.to_s
|
|
53
73
|
.tr("_", " ")
|
|
@@ -61,9 +81,14 @@ module Hecks
|
|
|
61
81
|
end.join(" ")
|
|
62
82
|
end
|
|
63
83
|
|
|
64
|
-
# "A, B, and C" / "A or B" —
|
|
65
|
-
# sentence a projection writes wants; lived in
|
|
66
|
-
# alone until a second projection needed it.
|
|
84
|
+
# Joins items into the Oxford-comma list — "A, B, and C" / "A or B" —
|
|
85
|
+
# every English sentence a projection writes wants; lived in
|
|
86
|
+
# `NarrateProjector` alone until a second projection needed it.
|
|
87
|
+
#
|
|
88
|
+
# @param items [Array<#to_s>] the items to join
|
|
89
|
+
# @param conj [String] the word before the last item, such as `"and"` or `"or"`
|
|
90
|
+
# @return [String] `""` for no items, the item's `to_s` for one, and the
|
|
91
|
+
# Oxford-comma join for more
|
|
67
92
|
def to_sentence_list(items, conj: "and")
|
|
68
93
|
case items.size
|
|
69
94
|
when 0 then ""
|
|
@@ -73,14 +98,17 @@ module Hecks
|
|
|
73
98
|
end
|
|
74
99
|
end
|
|
75
100
|
|
|
76
|
-
#
|
|
77
|
-
# `Projections::Statements#article`
|
|
78
|
-
# plain word, never "hour" or "university".
|
|
101
|
+
# Picks the article for `word` by its first letter — the vowel-letter
|
|
102
|
+
# heuristic, safe here for the same reason `Projections::Statements#article`
|
|
103
|
+
# gives: a construct name is a plain word, never "hour" or "university".
|
|
104
|
+
#
|
|
105
|
+
# @param word [String, Symbol, #to_s] the word the article precedes
|
|
106
|
+
# @return [String] `"an"` if `word` starts with a vowel letter, `"a"` otherwise
|
|
79
107
|
def a_or_an(word)
|
|
80
108
|
%w[a e i o u].include?(word.to_s[0].to_s.downcase) ? "an" : "a"
|
|
81
109
|
end
|
|
82
110
|
|
|
83
|
-
# The name a
|
|
111
|
+
# The name a collection of something takes.
|
|
84
112
|
#
|
|
85
113
|
# There were two of these and one was wrong. A read model's gathered heads
|
|
86
114
|
# derived their name with a bare `"#{snake(target)}s"`, so the meta-domain's
|
|
@@ -89,6 +117,9 @@ module Hecks
|
|
|
89
117
|
# the wrong rule against itself. Agreement is not correctness; it never was.
|
|
90
118
|
#
|
|
91
119
|
# So: one pluraliser, three rules, and every collection name flows through it.
|
|
120
|
+
#
|
|
121
|
+
# @param text [String, Symbol, #to_s] a singular name
|
|
122
|
+
# @return [String] the pluralised name
|
|
92
123
|
def plural(text)
|
|
93
124
|
word = text.to_s
|
|
94
125
|
return "#{word[0..-2]}ies" if word.match?(/[^aeiou]y\z/)
|
|
@@ -97,22 +128,25 @@ module Hecks
|
|
|
97
128
|
"#{word}s"
|
|
98
129
|
end
|
|
99
130
|
|
|
100
|
-
# `has_many`'s undo — the plural
|
|
131
|
+
# `has_many`'s undo — the plural written, back to the singular the target
|
|
101
132
|
# aggregate is actually named. Deliberately the crude half of a pair: `plural`
|
|
102
|
-
# above earns its precision (three suffix rules) because getting a
|
|
133
|
+
# above earns its precision (three suffix rules) because getting a collection
|
|
103
134
|
# name wrong reads as a typo forever ; this only ever recovers a name someone
|
|
104
135
|
# already wrote as a real aggregate, so "ies -> y, trailing s dropped" is the
|
|
105
136
|
# whole rule — enough for `has_many Invoices` to resolve to the aggregate
|
|
106
137
|
# actually named Invoice.
|
|
138
|
+
#
|
|
139
|
+
# @param text [String, Symbol, #to_s] a plural name
|
|
140
|
+
# @return [String] the singularised name
|
|
107
141
|
def singularize(text)
|
|
108
142
|
word = text.to_s
|
|
109
143
|
return "#{word[0..-4]}y" if word.length > 3 && word.end_with?("ies")
|
|
110
144
|
|
|
111
|
-
# `plural`'s
|
|
145
|
+
# `plural`'s own second rule adds "es" (not bare "s") after
|
|
112
146
|
# s/x/z/ch/sh — undone here the same way, or a word `plural`
|
|
113
147
|
# itself would have suffixed with "es" comes back missing its
|
|
114
148
|
# own trailing letter ("Boxes" -> "Boxe", not "Box") once this
|
|
115
|
-
# only ever knew how to drop a bare "s". Checked
|
|
149
|
+
# only ever knew how to drop a bare "s". Checked before the
|
|
116
150
|
# bare-"s" rule below: stripping "es" first and confirming what
|
|
117
151
|
# is left actually ends in one of those five shapes is what
|
|
118
152
|
# keeps an ordinary "-es" word (e.g. "Invoices" -> "Invoice")
|
|
@@ -124,38 +158,60 @@ module Hecks
|
|
|
124
158
|
word
|
|
125
159
|
end
|
|
126
160
|
|
|
161
|
+
# Derives the attribute name a reference to `type` is stored under.
|
|
162
|
+
#
|
|
163
|
+
# @param type [Module, String, Symbol, #to_s] the referenced construct's name
|
|
164
|
+
# or a `::`-joined path to it
|
|
165
|
+
# @return [Symbol] the snake_case, demodulised name, as a Symbol
|
|
127
166
|
def reference_key(type)
|
|
128
167
|
snake(demodulise(type)).to_sym
|
|
129
168
|
end
|
|
130
169
|
|
|
170
|
+
# Splits a `qualifier.name` string on its first dot.
|
|
171
|
+
#
|
|
172
|
+
# @param dotted [String, Symbol, #to_s] text, optionally containing a dot
|
|
173
|
+
# @return [Array(String, String)] `[before the first dot, after it]`; the
|
|
174
|
+
# second element is `""` when `dotted` has no dot
|
|
131
175
|
def split_dotted(dotted)
|
|
132
176
|
first, second = dotted.to_s.split(".", 2)
|
|
133
177
|
[first.to_s, second.to_s]
|
|
134
178
|
end
|
|
135
179
|
|
|
180
|
+
# The part of a dotted name before its first dot.
|
|
181
|
+
#
|
|
182
|
+
# @param dotted [String, Symbol, #to_s] text, optionally containing a dot
|
|
183
|
+
# @return [String, nil] the text before the first dot, or nil if `dotted`
|
|
184
|
+
# has no dot
|
|
136
185
|
def qualifier(dotted)
|
|
137
186
|
text = dotted.to_s
|
|
138
187
|
text.include?(".") ? text.split(".", 2).first : nil
|
|
139
188
|
end
|
|
140
189
|
|
|
190
|
+
# The part of a dotted name after its first dot.
|
|
191
|
+
#
|
|
192
|
+
# @param dotted [String, Symbol, #to_s] text, optionally containing a dot
|
|
193
|
+
# @return [String] the text after the first dot, or the whole text if
|
|
194
|
+
# `dotted` has no dot
|
|
141
195
|
def unqualified(dotted)
|
|
142
196
|
text = dotted.to_s
|
|
143
197
|
text.include?(".") ? text.split(".", 2).last : text
|
|
144
198
|
end
|
|
145
199
|
|
|
146
|
-
#
|
|
200
|
+
# Splits a domain-qualified verb into its domain, aggregate, and command parts.
|
|
201
|
+
#
|
|
202
|
+
# Domain, aggregate, then the REST dot-joined into one command path.
|
|
147
203
|
#
|
|
148
204
|
# The `::` boundary between domain and aggregate is unambiguous by
|
|
149
205
|
# construction — every caller here has already prefixed the domain
|
|
150
206
|
# itself (`PolicyInterpreter#deliver`, `SagaInterpreter#qualified`,
|
|
151
207
|
# `Router#dispatch`'s own rebuilt string) before this ever runs. A
|
|
152
|
-
#
|
|
208
|
+
# third `::` segment can still show up past that boundary: a bare
|
|
153
209
|
# `ScopedConstant` naming a port operation (`command_ref`'s own
|
|
154
210
|
# comment — `Aggregate::Port::Operation`, three colon-joined
|
|
155
|
-
# segments with no `.` of its own) only gets its
|
|
211
|
+
# segments with no `.` of its own) only gets its last `::` rewritten
|
|
156
212
|
# to `.` there, at DSL-build time, because nothing at that point
|
|
157
213
|
# knows yet whether the constant names a port operation or a
|
|
158
|
-
# domain-qualified command (`Domain::Aggregate::Command`, the
|
|
214
|
+
# domain-qualified command (`Domain::Aggregate::Command`, the other
|
|
159
215
|
# shape `command_ref` documents) — both are textually identical.
|
|
160
216
|
# Here, past the already-resolved domain boundary, any leftover
|
|
161
217
|
# `::` is unambiguous: it is that same rewrite artifact, and folding
|
|
@@ -166,6 +222,11 @@ module Hecks
|
|
|
166
222
|
# .kind_for` and `ReactionInvocation#resolve_target` both already
|
|
167
223
|
# assume this contract on their own end; this is what actually
|
|
168
224
|
# delivers it to them.
|
|
225
|
+
#
|
|
226
|
+
# @param verb [String, Symbol, #to_s] a `Domain::Aggregate.command` (or
|
|
227
|
+
# `.query`) path
|
|
228
|
+
# @return [Array(String, String, String), nil] `[domain, aggregate, command]`,
|
|
229
|
+
# or nil if `verb` has no `.` or no `domain::aggregate` before it
|
|
169
230
|
def split_verb(verb)
|
|
170
231
|
path, command = verb.to_s.split(".", 2)
|
|
171
232
|
return nil unless path && command
|
|
@@ -178,23 +239,30 @@ module Hecks
|
|
|
178
239
|
[domain, aggregate, command]
|
|
179
240
|
end
|
|
180
241
|
|
|
242
|
+
# Rewrites a bare command constant's trailing `::` into `.`, matching a
|
|
243
|
+
# command's own FQN separator; a String or Symbol passes through unchanged.
|
|
244
|
+
#
|
|
181
245
|
# `trigger Account::Debit` / `dispatch Account::Debit` — a bare
|
|
182
|
-
#
|
|
246
|
+
# constant reference (`ConstShim`'s own `ScopedConstant`, S0b), not
|
|
183
247
|
# text (ADR 0025, "events and reactions" — command references become
|
|
184
|
-
# first-class). Ruby's `::` joins
|
|
248
|
+
# first-class). Ruby's `::` joins every segment the same way a
|
|
185
249
|
# constant path always does, but a command's own `hecks_fqn` joins
|
|
186
250
|
# its aggregate with `.` (`Construct#hecks_separator`'s default,
|
|
187
|
-
# only an
|
|
251
|
+
# only an aggregate overrides it to `::`) — so only the last `::`
|
|
188
252
|
# becomes a `.`; everything before it (the chapter, when a domain is
|
|
189
253
|
# spelled at all: `Banking::Account::Debit`) stays `::`-joined.
|
|
190
254
|
#
|
|
191
|
-
# A
|
|
255
|
+
# A string passes through unchanged, on purpose — legacy era text
|
|
192
256
|
# (S0a's own shadow-parsed spelling) already mixes `::` (domain) and
|
|
193
257
|
# `.` (command) correctly on its own, e.g. `"Banking::Account.Debit"`,
|
|
194
|
-
# and re-splitting that by content rather than by
|
|
258
|
+
# and re-splitting that by content rather than by type would corrupt
|
|
195
259
|
# it (its own last `::` sits between the domain and the aggregate,
|
|
196
260
|
# not the aggregate and the command). Only an actual constant object
|
|
197
261
|
# — never seen holding a `.` of its own — needs the rewrite at all.
|
|
262
|
+
#
|
|
263
|
+
# @param value [Symbol, String, Module] the command, as a bare constant (a
|
|
264
|
+
# `ScopedConstant` module `ConstShim` resolves) or already-dotted text
|
|
265
|
+
# @return [String] the dotted command reference
|
|
198
266
|
def command_ref(value)
|
|
199
267
|
return value.to_s if value.is_a?(::String) || value.is_a?(::Symbol)
|
|
200
268
|
|
|
@@ -203,6 +271,9 @@ module Hecks
|
|
|
203
271
|
path.empty? ? text : "#{path}.#{command}"
|
|
204
272
|
end
|
|
205
273
|
|
|
274
|
+
# The event-side twin of `command_ref`, kept as its own name because an
|
|
275
|
+
# event name is not a command name that happens to share a format.
|
|
276
|
+
#
|
|
206
277
|
# `emits Account::AccountFrozen` / `on Account::AccountFrozen` — the
|
|
207
278
|
# event-side twin of `command_ref`, above (ADR 0025, S6 — "events
|
|
208
279
|
# first-class"). Identical transform (a bare `ScopedConstant`'s last
|
|
@@ -212,34 +283,44 @@ module Hecks
|
|
|
212
283
|
# own name because the two references mean different things even
|
|
213
284
|
# though the rewrite is byte-identical: an event name is not a
|
|
214
285
|
# command name that happens to share a format.
|
|
286
|
+
#
|
|
287
|
+
# @param value [Symbol, String, Module] the event, as a bare constant (a
|
|
288
|
+
# `ScopedConstant` module `ConstShim` resolves) or already-dotted text
|
|
289
|
+
# @return [String] the dotted event reference
|
|
215
290
|
def event_ref(value) = command_ref(value)
|
|
216
291
|
|
|
292
|
+
# Strips a process manager's event reference down to its bare event name.
|
|
293
|
+
#
|
|
217
294
|
# `transition Account::AccountDebited => "state"` / `starts_on
|
|
218
295
|
# Transfer::TransferRequested` / `ends_on Transfer::TransferSettled`
|
|
219
|
-
# — a process manager's
|
|
220
|
-
#
|
|
296
|
+
# — a process manager's own event references (ADR 0025, S6),
|
|
297
|
+
# deliberately not `event_ref` — found live, not assumed, wiring a
|
|
221
298
|
# real migrated corpus site into `bin/model_check` for the first
|
|
222
299
|
# time (2026-08-28): `SagaInterpreter#begin_saga`/`#advance_saga`
|
|
223
300
|
# match `pm.starts_on`/`pm.handler_for` against `event.name`, which
|
|
224
|
-
# `CommandRules::Emission#emit` stamps
|
|
301
|
+
# `CommandRules::Emission#emit` stamps bare — a command's own
|
|
225
302
|
# `emits AccountDebited` never carries its aggregate's name at all
|
|
226
303
|
# (unlike a policy's cross-aggregate `on`, matched instead by
|
|
227
304
|
# `Naming.demodulise(event.aggregate)` split apart from the bare
|
|
228
305
|
# name — `PolicyInterpreter#policies_for`). Handing a saga's own
|
|
229
|
-
# matcher the
|
|
306
|
+
# matcher the dotted `event_ref` form ("Account.AccountDebited")
|
|
230
307
|
# would silently name an event no command in the domain ever
|
|
231
308
|
# actually emits — caught by `bin/model_check`'s own `deaf_handler`/
|
|
232
309
|
# `deaf_trigger` findings the moment a real qualified corpus site
|
|
233
310
|
# existed to trip them, not by any unit test in isolation.
|
|
234
311
|
#
|
|
235
|
-
# A qualifier is still worth
|
|
312
|
+
# A qualifier is still worth writing (`Account::`) — the same
|
|
236
313
|
# provenance a reader gets from `trigger Account::Debit` — it is
|
|
237
|
-
# only not worth
|
|
314
|
+
# only not worth keeping: `demodulise` drops everything but the
|
|
238
315
|
# final segment, so `Account::AccountDebited` and a bare
|
|
239
316
|
# `AccountDebited` resolve to the identical stored string. A String
|
|
240
317
|
# passes through unchanged either way, exactly like `command_ref`'s
|
|
241
318
|
# own legacy branch — this corpus never spelled one dotted to begin
|
|
242
319
|
# with, so there is nothing here to strip.
|
|
320
|
+
#
|
|
321
|
+
# @param value [Symbol, String, Module] the event, as a bare constant (a
|
|
322
|
+
# `ScopedConstant` module `ConstShim` resolves) or already-dotted text
|
|
323
|
+
# @return [String] the bare event name, with any qualifier stripped
|
|
243
324
|
def event_name_ref(value) = demodulise(value)
|
|
244
325
|
end
|
|
245
326
|
end
|
|
@@ -2,7 +2,7 @@ require_relative "../runtime/registry"
|
|
|
2
2
|
|
|
3
3
|
module Hecks
|
|
4
4
|
module Ports
|
|
5
|
-
#
|
|
5
|
+
# Who can sign in, and what role do they hold — the domain-specific
|
|
6
6
|
# half of sign-in `Ports::Authentication` deliberately stays out of
|
|
7
7
|
# (see that port's own header: it only ever talks to the external
|
|
8
8
|
# provider, never this registry's own data). Resolved the same way
|
|
@@ -13,7 +13,7 @@ module Hecks
|
|
|
13
13
|
# Every domain shapes this differently — who its own "a person who
|
|
14
14
|
# can sign in" aggregate is, what admits them, what grants them
|
|
15
15
|
# access — so this port is pure delegation, same as every sibling
|
|
16
|
-
# port. The one piece of this that
|
|
16
|
+
# port. The one piece of this that is generic (a live Governance
|
|
17
17
|
# grant should win over whatever an aggregate's own role field
|
|
18
18
|
# says) lives on `Ports::Authorization#live_role_for` instead —
|
|
19
19
|
# a domain's adapter here calls that directly, rather than this
|
|
@@ -23,26 +23,77 @@ module Hecks
|
|
|
23
23
|
|
|
24
24
|
module_function
|
|
25
25
|
|
|
26
|
+
# Asks the domain's adapter for the session an already-resolved identity signs into.
|
|
27
|
+
#
|
|
28
|
+
# No adapter or spec double for this port ships in this repository, so every shape
|
|
29
|
+
# below other than `registry` is adapter-defined: the port forwards it untouched.
|
|
30
|
+
#
|
|
31
|
+
# @param registry [Runtime::Registry] the booted registry to resolve the adapter against
|
|
32
|
+
# @param identity_id [Object] adapter-defined identity key, forwarded unchanged
|
|
33
|
+
# @return [Object] adapter-defined session representation
|
|
34
|
+
# @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
|
|
35
|
+
# (see `adapter`)
|
|
26
36
|
def session_for_identity(registry, identity_id:)
|
|
27
37
|
adapter(registry).session_for_identity(registry, identity_id: identity_id)
|
|
28
38
|
end
|
|
29
39
|
|
|
40
|
+
# Admits a new person, in whatever way this domain's adapter defines admission.
|
|
41
|
+
#
|
|
42
|
+
# The keywords carry the names of a verified sign-in (`Ports::Authentication.verify`
|
|
43
|
+
# answers `issuer`, `subject` and `email`), but nothing in this repository wires the
|
|
44
|
+
# two together, so their shapes here are adapter-defined.
|
|
45
|
+
#
|
|
46
|
+
# @param registry [Runtime::Registry] the booted registry to resolve the adapter against
|
|
47
|
+
# @param email [Object] adapter-defined, forwarded unchanged; the person's email
|
|
48
|
+
# @param issuer [Object] adapter-defined, forwarded unchanged; the OIDC issuer that
|
|
49
|
+
# authenticated the person
|
|
50
|
+
# @param subject [Object] adapter-defined, forwarded unchanged; the OIDC subject the
|
|
51
|
+
# issuer vouches for
|
|
52
|
+
# @return [Object] adapter-defined representation of the newly admitted person
|
|
53
|
+
# @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
|
|
54
|
+
# (see `adapter`)
|
|
30
55
|
def provision(registry, email:, issuer:, subject:)
|
|
31
56
|
adapter(registry).provision(registry, email: email, issuer: issuer, subject: subject)
|
|
32
57
|
end
|
|
33
58
|
|
|
59
|
+
# Asks the domain's adapter which roles it can grant.
|
|
60
|
+
#
|
|
61
|
+
# @param registry [Runtime::Registry] the booted registry to resolve the adapter against
|
|
62
|
+
# @return [Object] adapter-defined collection of grantable roles
|
|
63
|
+
# @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
|
|
64
|
+
# (see `adapter`)
|
|
34
65
|
def available_roles(registry)
|
|
35
66
|
adapter(registry).available_roles(registry)
|
|
36
67
|
end
|
|
37
68
|
|
|
69
|
+
# Grants a role to a person, by whatever means the domain's adapter records a grant.
|
|
70
|
+
#
|
|
71
|
+
# @param registry [Runtime::Registry] the booted registry to resolve the adapter against
|
|
72
|
+
# @param email [Object] adapter-defined, forwarded unchanged; the person receiving the role
|
|
73
|
+
# @param role [Object] adapter-defined, forwarded unchanged; the role to grant
|
|
74
|
+
# @return [Object] adapter-defined representation of the grant
|
|
75
|
+
# @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
|
|
76
|
+
# (see `adapter`)
|
|
38
77
|
def grant(registry, email:, role:)
|
|
39
78
|
adapter(registry).grant(registry, email: email, role: role)
|
|
40
79
|
end
|
|
41
80
|
|
|
81
|
+
# Lists every person the domain's adapter knows about.
|
|
82
|
+
#
|
|
83
|
+
# @param registry [Runtime::Registry] the booted registry to resolve the adapter against
|
|
84
|
+
# @return [Object] adapter-defined collection of people
|
|
85
|
+
# @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
|
|
86
|
+
# (see `adapter`)
|
|
42
87
|
def all_people(registry)
|
|
43
88
|
adapter(registry).all_people(registry)
|
|
44
89
|
end
|
|
45
90
|
|
|
91
|
+
# Finds the single adapter bound to this port, refusing an ambiguous wiring.
|
|
92
|
+
#
|
|
93
|
+
# @param registry [Runtime::Registry] the booted registry to search
|
|
94
|
+
# @return [Module] the adapter module or class implementing this port
|
|
95
|
+
# @raise [Runtime::WiringError] if no adapter, or more than one, implements this port,
|
|
96
|
+
# or the one that does has no Ruby implementation under `Hecks::Adapters`
|
|
46
97
|
def adapter(registry)
|
|
47
98
|
implementations = registry.adapters.values.select { |a| a.port == NAME }
|
|
48
99
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
module Hecks
|
|
2
2
|
module Ports
|
|
3
3
|
module Agent
|
|
4
|
-
#
|
|
4
|
+
# Where a raw hash becomes a struct — the one place, so every
|
|
5
5
|
# adapter (the real `claude_code` one, and any scripted double
|
|
6
6
|
# standing in for it in a spec) is held to the identical shape.
|
|
7
7
|
# An adapter's whole job ends at "here is what came back, already
|
|
8
|
-
# JSON"; whether that hash is
|
|
8
|
+
# JSON"; whether that hash is usable is decided once, here, not
|
|
9
9
|
# re-decided per adapter.
|
|
10
10
|
#
|
|
11
11
|
# Every failure raises `ValidationError` — never lets a bare
|
|
@@ -18,16 +18,43 @@ module Hecks
|
|
|
18
18
|
|
|
19
19
|
module_function
|
|
20
20
|
|
|
21
|
+
# Validates an adapter's raw answer to `ask` into `Question` structs.
|
|
22
|
+
#
|
|
23
|
+
# @param raw [Hash{String => Object}] the adapter's parsed reply, expected to hold a
|
|
24
|
+
# `"questions"` array of `{"text" =>, "because" =>}` rows; any other shape is refused
|
|
25
|
+
# @return [Array<Ports::Agent::Question>] one struct per row, in the answer's order;
|
|
26
|
+
# `[]` for an empty `"questions"` array
|
|
27
|
+
# @raise [Ports::Agent::ValidationError] if `raw` is not a Hash, holds no
|
|
28
|
+
# `"questions"` array, or a row's `"text"` or `"because"` is missing or blank
|
|
21
29
|
def questions(raw)
|
|
22
30
|
rows(raw, "questions").map { |row| Question.new(text: text!(row, "text"), because: text!(row, "because")) }
|
|
23
31
|
end
|
|
24
32
|
|
|
33
|
+
# Validates an adapter's raw answer to `interpret` into `Proposal` structs.
|
|
34
|
+
#
|
|
35
|
+
# @param raw [Hash{String => Object}] the adapter's parsed reply, expected to hold a
|
|
36
|
+
# `"proposals"` array of `{"verb" =>, "rationale" =>, "arguments" =>}` rows
|
|
37
|
+
# @return [Array<Ports::Agent::Proposal>] one struct per row, its `arguments` an Array
|
|
38
|
+
# of `{name:, field:, value:}` Hashes (see `arguments!`); `[]` for an empty
|
|
39
|
+
# `"proposals"` array
|
|
40
|
+
# @raise [Ports::Agent::ValidationError] if `raw` is not a Hash, holds no
|
|
41
|
+
# `"proposals"` array, or a row has a verb that is not fully qualified, a blank
|
|
42
|
+
# `"rationale"`, or an argument row with no `"name"`
|
|
25
43
|
def proposals(raw)
|
|
26
44
|
rows(raw, "proposals").map do |row|
|
|
27
45
|
Proposal.new(verb: verb!(row), rationale: text!(row, "rationale"), arguments: arguments!(row))
|
|
28
46
|
end
|
|
29
47
|
end
|
|
30
48
|
|
|
49
|
+
# Validates an adapter's raw answer to `critique` into `Finding` structs.
|
|
50
|
+
#
|
|
51
|
+
# @param raw [Hash{String => Object}] the adapter's parsed reply, expected to hold a
|
|
52
|
+
# `"findings"` array of `{"kind" =>, "severity" =>, "subject" =>, "message" =>}` rows
|
|
53
|
+
# @return [Array<Ports::Agent::Finding>] one struct per row, with `kind` and `severity`
|
|
54
|
+
# as Symbols; `[]` for an empty `"findings"` array
|
|
55
|
+
# @raise [Ports::Agent::ValidationError] if `raw` is not a Hash, holds no
|
|
56
|
+
# `"findings"` array, or a row's kind is outside `CRITIQUE_KINDS`, its severity is
|
|
57
|
+
# outside `SEVERITIES`, or its `"subject"` or `"message"` is missing or blank
|
|
31
58
|
def findings(raw)
|
|
32
59
|
rows(raw, "findings").map do |row|
|
|
33
60
|
Finding.new(kind: kind!(row), severity: severity!(row), subject: text!(row, "subject"),
|
|
@@ -35,6 +62,14 @@ module Hecks
|
|
|
35
62
|
end
|
|
36
63
|
end
|
|
37
64
|
|
|
65
|
+
# Validates an adapter's raw answer to `suggest_name` into `Suggestion` structs.
|
|
66
|
+
#
|
|
67
|
+
# @param raw [Hash{String => Object}] the adapter's parsed reply, expected to hold a
|
|
68
|
+
# `"names"` array of `{"name" =>, "because" =>, "rejected" =>}` rows
|
|
69
|
+
# @return [Array<Ports::Agent::Suggestion>] one struct per row, its `rejected` an
|
|
70
|
+
# Array of Strings (`[]` when the row carries none); `[]` for an empty `"names"` array
|
|
71
|
+
# @raise [Ports::Agent::ValidationError] if `raw` is not a Hash, holds no `"names"`
|
|
72
|
+
# array, or a row's `"name"` or `"because"` is missing or blank
|
|
38
73
|
def suggestions(raw)
|
|
39
74
|
rows(raw, "names").map do |row|
|
|
40
75
|
Suggestion.new(name: text!(row, "name"), because: text!(row, "because"),
|
|
@@ -44,6 +79,13 @@ module Hecks
|
|
|
44
79
|
|
|
45
80
|
# ── shared checks, each named for what it refuses ───────────────
|
|
46
81
|
|
|
82
|
+
# Reads the array of rows an answer holds under one key, refusing any other shape.
|
|
83
|
+
#
|
|
84
|
+
# @param raw [Object] the adapter's reply; only a Hash is accepted
|
|
85
|
+
# @param key [String] the key the rows sit under, such as `"questions"`
|
|
86
|
+
# @return [Array<Object>] the rows exactly as the adapter gave them, unvalidated
|
|
87
|
+
# @raise [Ports::Agent::ValidationError] if `raw` is not a Hash, or `raw[key]` is
|
|
88
|
+
# not an Array
|
|
47
89
|
def rows(raw, key)
|
|
48
90
|
raise ValidationError, "expected a Hash back, got #{raw.class}: #{raw.inspect}" unless raw.is_a?(Hash)
|
|
49
91
|
|
|
@@ -53,6 +95,13 @@ module Hecks
|
|
|
53
95
|
raise ValidationError, "no #{key.inspect} array in the answer: #{raw.inspect}"
|
|
54
96
|
end
|
|
55
97
|
|
|
98
|
+
# Reads one required text field off a row, refusing a missing or blank one.
|
|
99
|
+
#
|
|
100
|
+
# @param row [Hash{String => Object}] one row of the adapter's answer
|
|
101
|
+
# @param key [String] the field to read, such as `"because"`
|
|
102
|
+
# @return [String] the field's value as a String, whitespace untouched
|
|
103
|
+
# @raise [Ports::Agent::ValidationError] if the field is nil, false, empty, or only
|
|
104
|
+
# whitespace
|
|
56
105
|
def text!(row, key)
|
|
57
106
|
value = row[key]
|
|
58
107
|
return value.to_s if value && !value.to_s.strip.empty?
|
|
@@ -60,12 +109,19 @@ module Hecks
|
|
|
60
109
|
raise ValidationError, "#{key.inspect} missing or blank in #{row.inspect}"
|
|
61
110
|
end
|
|
62
111
|
|
|
63
|
-
#
|
|
112
|
+
# Reads a proposal row's verb, refusing one that is not fully qualified.
|
|
113
|
+
#
|
|
114
|
+
# A verb the language could not even parse is an adapter fault,
|
|
64
115
|
# refused right here — the same `not_fully_qualified` shape the
|
|
65
116
|
# language's own grammar already refuses by, reused rather than
|
|
66
117
|
# reinvented. A verb naming a real category that turns out to
|
|
67
|
-
# describe the wrong fact is
|
|
118
|
+
# describe the wrong fact is not this port's business: that one
|
|
68
119
|
# dispatches, and `Interview::Session#offer` is what says no.
|
|
120
|
+
#
|
|
121
|
+
# @param row [Hash{String => Object}] one proposal row of the adapter's answer
|
|
122
|
+
# @return [String] the row's `"verb"`, shaped `Chapter::Aggregate.Command`
|
|
123
|
+
# @raise [Ports::Agent::ValidationError] if the verb is missing or does not match
|
|
124
|
+
# `VERB_PATTERN`
|
|
69
125
|
def verb!(row)
|
|
70
126
|
verb = row["verb"].to_s
|
|
71
127
|
return verb if VERB_PATTERN.match?(verb)
|
|
@@ -73,6 +129,12 @@ module Hecks
|
|
|
73
129
|
raise ValidationError, "#{verb.inspect} is not a fully-qualified verb (Chapter::Aggregate.Command)"
|
|
74
130
|
end
|
|
75
131
|
|
|
132
|
+
# Reads a finding row's kind, refusing one outside the closed critique vocabulary.
|
|
133
|
+
#
|
|
134
|
+
# @param row [Hash{String => Object}] one finding row of the adapter's answer
|
|
135
|
+
# @return [Symbol] the row's `"kind"`, a member of `CRITIQUE_KINDS`
|
|
136
|
+
# @raise [Ports::Agent::ValidationError] if the kind is missing or not in
|
|
137
|
+
# `CRITIQUE_KINDS`
|
|
76
138
|
def kind!(row)
|
|
77
139
|
kind = row["kind"].to_s.to_sym
|
|
78
140
|
return kind if CRITIQUE_KINDS.include?(kind)
|
|
@@ -80,6 +142,12 @@ module Hecks
|
|
|
80
142
|
raise ValidationError, "#{kind.inspect} is not a critique kind this port knows (#{CRITIQUE_KINDS.join(', ')})"
|
|
81
143
|
end
|
|
82
144
|
|
|
145
|
+
# Reads a finding row's severity, refusing anything but the two that exist.
|
|
146
|
+
#
|
|
147
|
+
# @param row [Hash{String => Object}] one finding row of the adapter's answer
|
|
148
|
+
# @return [Symbol] the row's `"severity"`, a member of `SEVERITIES`
|
|
149
|
+
# @raise [Ports::Agent::ValidationError] if the severity is missing or not in
|
|
150
|
+
# `SEVERITIES`
|
|
83
151
|
def severity!(row)
|
|
84
152
|
severity = row["severity"].to_s.to_sym
|
|
85
153
|
return severity if SEVERITIES.include?(severity)
|
|
@@ -87,9 +155,18 @@ module Hecks
|
|
|
87
155
|
raise ValidationError, "#{severity.inspect} is not a severity this port knows (#{SEVERITIES.join(', ')})"
|
|
88
156
|
end
|
|
89
157
|
|
|
90
|
-
#
|
|
91
|
-
#
|
|
158
|
+
# Normalises a proposal row's arguments into symbol-keyed, all-String rows.
|
|
159
|
+
#
|
|
160
|
+
# Rows shaped exactly as `Interview::Proposal::Argument` — a
|
|
161
|
+
# reference's `field` is legitimately blank (a reference is an
|
|
92
162
|
# id), so only `name` is required here.
|
|
163
|
+
#
|
|
164
|
+
# @param row [Hash{String => Object}] one proposal row of the adapter's answer
|
|
165
|
+
# @return [Array<Hash{Symbol => String}>] one `{name:, field:, value:}` Hash per
|
|
166
|
+
# argument, `field` and `value` being `""` when absent; `[]` when the row has no
|
|
167
|
+
# `"arguments"`
|
|
168
|
+
# @raise [Ports::Agent::ValidationError] if an argument row's `"name"` is missing or
|
|
169
|
+
# empty
|
|
93
170
|
def arguments!(row)
|
|
94
171
|
Array(row["arguments"]).map do |argument|
|
|
95
172
|
name = argument["name"].to_s
|