hecks 1.2.0 → 1.4.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 +7 -7
- data/lib/hecks/adapters/driven/d1.rb +187 -23
- data/lib/hecks/adapters/driven/folder.rb +10 -10
- data/lib/hecks/adapters/driven/google_authentication.rb +8 -8
- data/lib/hecks/adapters/driven/governance_authorization.rb +31 -13
- data/lib/hecks/adapters/driven/heki/journal.rb +60 -2
- data/lib/hecks/adapters/driven/heki/saga_store.rb +5 -5
- data/lib/hecks/adapters/driven/heki.rb +13 -7
- data/lib/hecks/adapters/driven/identity_registry.rb +2 -2
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +3 -3
- data/lib/hecks/adapters/driven/lambda/client.rb +34 -9
- data/lib/hecks/adapters/driven/lambda.rb +39 -33
- data/lib/hecks/adapters/driven/local_storage.rb +17 -10
- data/lib/hecks/adapters/driven/memory.rb +205 -9
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +1 -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/postgres_era.adapter +5 -0
- data/lib/hecks/adapters/driven/prism.rb +4 -4
- 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.rb +4 -4
- data/lib/hecks/adapters/driving/github_webhook.rb +145 -0
- data/lib/hecks/behaviors/dsl.rb +2 -2
- data/lib/hecks/behaviors/expectations.rb +51 -23
- data/lib/hecks/behaviors/ir.rb +1 -1
- data/lib/hecks/behaviors/rspec.rb +1 -1
- data/lib/hecks/behaviors/runner.rb +2 -2
- data/lib/hecks/behaviors.rb +1 -1
- data/lib/hecks/bluebook/aggregate.rb +13 -13
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +10 -10
- data/lib/hecks/bluebook/assembly/build.rb +1 -1
- data/lib/hecks/bluebook/assembly/contract.rb +39 -16
- data/lib/hecks/bluebook/assembly/contracts.rb +40 -38
- data/lib/hecks/bluebook/assembly/marks.rb +18 -18
- data/lib/hecks/bluebook/assembly/specializer.rb +19 -21
- data/lib/hecks/bluebook/assembly.rb +9 -9
- data/lib/hecks/bluebook/attribute.rb +9 -9
- data/lib/hecks/bluebook/behaviour/aggregate.rb +11 -11
- data/lib/hecks/bluebook/behaviour/attribute.rb +5 -5
- data/lib/hecks/bluebook/behaviour/chapter.rb +23 -5
- data/lib/hecks/bluebook/behaviour/command.rb +23 -23
- data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -3
- data/lib/hecks/bluebook/behaviour/entity.rb +8 -8
- data/lib/hecks/bluebook/behaviour/hexagon.rb +4 -4
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +5 -5
- data/lib/hecks/bluebook/behaviour/policy.rb +12 -12
- data/lib/hecks/bluebook/behaviour/process_manager.rb +7 -7
- data/lib/hecks/bluebook/behaviour/query.rb +1 -1
- data/lib/hecks/bluebook/behaviour/read_model.rb +8 -8
- data/lib/hecks/bluebook/behaviour/traits.rb +12 -12
- data/lib/hecks/bluebook/behaviour/value_object.rb +6 -6
- data/lib/hecks/bluebook/capabilities.rb +27 -0
- data/lib/hecks/bluebook/chapter.rb +28 -9
- data/lib/hecks/bluebook/command.rb +12 -12
- data/lib/hecks/bluebook/domain_port.rb +9 -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 +96 -96
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +41 -41
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +22 -2
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +111 -74
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +48 -30
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +103 -103
- 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 +56 -56
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +89 -30
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +17 -17
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
- data/lib/hecks/bluebook/dsl/policy_builder.rb +30 -21
- 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 +35 -35
- data/lib/hecks/bluebook/dsl/query_builder.rb +5 -5
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +34 -34
- data/lib/hecks/bluebook/dsl/rule_reference.rb +41 -39
- data/lib/hecks/bluebook/dsl/translation_builder.rb +9 -9
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +16 -16
- 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 +11 -11
- data/lib/hecks/bluebook/expression/ast_json.rb +20 -20
- data/lib/hecks/bluebook/expression/ast_reader.rb +3 -3
- data/lib/hecks/bluebook/expression/canonical_form.rb +9 -9
- data/lib/hecks/bluebook/expression/evaluator.rb +18 -18
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +18 -18
- data/lib/hecks/bluebook/expression/resolver.rb +60 -62
- data/lib/hecks/bluebook/hexagon.rb +1 -1
- data/lib/hecks/bluebook/lifecycle.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/judge.rb +122 -86
- data/lib/hecks/bluebook/meta_validator/plan.rb +39 -39
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -2
- data/lib/hecks/bluebook/meta_validator/readings.rb +43 -43
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +44 -37
- data/lib/hecks/bluebook/meta_validator/shapes.rb +25 -21
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +145 -31
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +6 -6
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +5 -5
- data/lib/hecks/bluebook/meta_validator.rb +70 -70
- data/lib/hecks/bluebook/model_check.rb +301 -84
- data/lib/hecks/bluebook/pattern_subset.rb +9 -9
- data/lib/hecks/bluebook/policy.rb +15 -13
- data/lib/hecks/bluebook/process_manager.rb +14 -14
- data/lib/hecks/bluebook/project_register.rb +6 -6
- data/lib/hecks/bluebook/query.rb +4 -4
- data/lib/hecks/bluebook/read_model.rb +14 -14
- data/lib/hecks/bluebook/reference.rb +8 -8
- data/lib/hecks/bluebook/smoke_test.rb +19 -19
- data/lib/hecks/bluebook/synthesizer.rb +12 -12
- data/lib/hecks/bluebook/translation.rb +4 -4
- data/lib/hecks/bluebook/value_object.rb +6 -6
- data/lib/hecks/bluebook.rb +2 -2
- data/lib/hecks/codemod/legacy_dispatch_args.rb +299 -0
- data/lib/hecks/codemod/legacy_dispatch_recorder.rb +186 -0
- data/lib/hecks/codemod.rb +36 -35
- data/lib/hecks/construct.rb +6 -6
- data/lib/hecks/corpus.rb +317 -0
- data/lib/hecks/deprecation.rb +95 -0
- data/lib/hecks/doc/reference.rb +19 -19
- data/lib/hecks/embryonaut_bluebook.rb +11 -11
- 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 +79 -32
- data/lib/hecks/facade/json_door.rb +106 -25
- data/lib/hecks/facade/surface/aggregate_door.rb +42 -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 -20
- data/lib/hecks/forms/html.rb +51 -7
- data/lib/hecks/forms/index_renderer.rb +14 -2
- data/lib/hecks/forms/params.rb +120 -23
- data/lib/hecks/forms/port_argument.rb +2 -2
- data/lib/hecks/forms/query_form_renderer.rb +2 -2
- data/lib/hecks/forms/record_renderer.rb +2 -2
- data/lib/hecks/forms/record_table.rb +1 -1
- data/lib/hecks/forms/value_object_shape.rb +3 -3
- data/lib/hecks/forms.rb +24 -4
- data/lib/hecks/fqn.rb +1 -1
- data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
- data/lib/hecks/framework.rb +48 -17
- data/lib/hecks/freezer.rb +11 -11
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +90 -90
- data/lib/hecks/fuzzing/combination_miner.rb +119 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +361 -0
- data/lib/hecks/fuzzing/coverage_campaign.rb +118 -0
- data/lib/hecks/fuzzing/differential.rb +158 -0
- data/lib/hecks/fuzzing/domain_generator.rb +694 -0
- data/lib/hecks/fuzzing/era_boundary.rb +124 -0
- data/lib/hecks/fuzzing/form_census.rb +199 -0
- data/lib/hecks/fuzzing/generated_domain_check.rb +95 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +6 -6
- data/lib/hecks/fuzzing/isolated_boot.rb +226 -38
- data/lib/hecks/fuzzing/nondeterministic.rb +67 -0
- data/lib/hecks/fuzzing/persistence_parity.rb +161 -0
- data/lib/hecks/fuzzing/properties/corrections.rb +100 -0
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +321 -41
- data/lib/hecks/fuzzing/properties/guards.rb +129 -26
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +32 -32
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +31 -11
- data/lib/hecks/fuzzing/properties/outbox.rb +142 -0
- data/lib/hecks/fuzzing/properties/querying.rb +17 -17
- data/lib/hecks/fuzzing/properties.rb +84 -39
- data/lib/hecks/fuzzing/qa_settings.rb +152 -0
- data/lib/hecks/fuzzing/replay.rb +224 -114
- data/lib/hecks/fuzzing/rotation_priority.rb +94 -0
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +113 -0
- data/lib/hecks/fuzzing/self_consistency.rb +676 -0
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +526 -0
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +101 -30
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +55 -8
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +31 -11
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +123 -32
- data/lib/hecks/fuzzing/sequence_generator.rb +152 -31
- data/lib/hecks/fuzzing/shrinker.rb +197 -0
- data/lib/hecks/fuzzing/structural_skips.rb +39 -0
- data/lib/hecks/fuzzing/sweep_depth.rb +53 -0
- data/lib/hecks/fuzzing/target_capabilities.rb +180 -0
- data/lib/hecks/fuzzing/value_generator.rb +66 -14
- data/lib/hecks/fuzzing.rb +11 -0
- data/lib/hecks/grammar/evolve.rb +10 -10
- data/lib/hecks/grammar.rb +7 -7
- data/lib/hecks/ir.rb +13 -13
- 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 +365 -15
- data/lib/hecks/language/oidc.json +5 -0
- data/lib/hecks/literal.rb +9 -9
- data/lib/hecks/naming.rb +89 -21
- data/lib/hecks/ports/access_control.rb +58 -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 +53 -11
- data/lib/hecks/ports/clock.rb +42 -23
- data/lib/hecks/ports/extraction.rb +16 -0
- data/lib/hecks/ports/identity_assignment.rb +24 -2
- data/lib/hecks/ports/identity_generation.rb +17 -3
- data/lib/hecks/ports/identity_resolution.rb +18 -1
- 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 +160 -58
- 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 +28 -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 +44 -6
- 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 +57 -4
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +338 -83
- 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 +42 -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 +18 -19
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +5 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +1 -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 +29 -6
- 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 +3 -3
- data/lib/hecks/ports/query/ordering.rb +6 -6
- data/lib/hecks/ports/query.rb +35 -0
- data/lib/hecks/projections/bootstrap_table.rb +112 -0
- data/lib/hecks/projections/diagrams.rb +75 -75
- data/lib/hecks/projections/glossary/html.rb +250 -0
- data/lib/hecks/projections/glossary/markdown.rb +105 -0
- data/lib/hecks/projections/glossary/mermaid.rb +110 -0
- data/lib/hecks/projections/glossary/page.css +271 -0
- data/lib/hecks/projections/glossary/page.js +72 -0
- data/lib/hecks/projections/glossary/sections.rb +17 -0
- data/lib/hecks/projections/glossary/sentences.rb +205 -0
- data/lib/hecks/projections/glossary.rb +214 -286
- data/lib/hecks/projections/ir.rb +1 -1
- data/lib/hecks/projections/model/deviations.rb +18 -17
- data/lib/hecks/projections/model.rb +25 -21
- data/lib/hecks/projections/oidc.rb +7 -7
- data/lib/hecks/projections/parser_table.rb +5 -5
- data/lib/hecks/projections/reference.rb +3 -3
- data/lib/hecks/projections/rust_vocabulary.rb +443 -0
- data/lib/hecks/projections/shape.rb +2 -2
- data/lib/hecks/projections/statements.rb +11 -11
- data/lib/hecks/projections/vocabulary.rb +9 -9
- data/lib/hecks/projections.rb +5 -3
- data/lib/hecks/projector/cli_projector.rb +29 -29
- data/lib/hecks/projector/docs_projector.rb +13 -13
- data/lib/hecks/projector/exporter.rb +42 -21
- data/lib/hecks/projector/ir_projector.rb +1 -1
- data/lib/hecks/projector/narrate_projector.rb +15 -22
- data/lib/hecks/projector/target.rb +13 -13
- data/lib/hecks/projector.rb +15 -15
- data/lib/hecks/query_ir.rb +47 -47
- data/lib/hecks/query_specification/common/comparators.rb +19 -3
- data/lib/hecks/query_specification/common/comparison.rb +132 -24
- data/lib/hecks/query_specification/common/dsl.rb +65 -9
- data/lib/hecks/query_specification/common/null_policy.rb +57 -13
- 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 +3 -3
- data/lib/hecks/router/namespace_installer.rb +3 -3
- data/lib/hecks/router.rb +1 -1
- data/lib/hecks/runtime/aggregate_lock.rb +11 -11
- data/lib/hecks/runtime/boot_gates.rb +3 -3
- data/lib/hecks/runtime/caller.rb +8 -8
- data/lib/hecks/runtime/capability_graph.rb +2 -2
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +22 -25
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +64 -67
- data/lib/hecks/runtime/command_interpreter.rb +159 -102
- data/lib/hecks/runtime/command_rules/admissibility.rb +69 -69
- data/lib/hecks/runtime/command_rules/arithmetic.rb +137 -91
- 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 +94 -19
- data/lib/hecks/runtime/command_rules.rb +3 -0
- data/lib/hecks/runtime/dependency_planning.rb +11 -11
- data/lib/hecks/runtime/dispatcher.rb +283 -90
- data/lib/hecks/runtime/entity_element.rb +282 -46
- data/lib/hecks/runtime/entity_interpreter.rb +160 -69
- data/lib/hecks/runtime/errors.rb +19 -19
- data/lib/hecks/runtime/event.rb +6 -6
- data/lib/hecks/runtime/identity.rb +22 -22
- data/lib/hecks/runtime/instance.rb +39 -14
- data/lib/hecks/runtime/interpreting.rb +12 -12
- data/lib/hecks/runtime/invocation.rb +276 -0
- data/lib/hecks/runtime/loader.rb +14 -14
- data/lib/hecks/runtime/outbox.rb +23 -23
- data/lib/hecks/runtime/policy_interpreter.rb +54 -54
- data/lib/hecks/runtime/port_operation_interpreter.rb +22 -19
- data/lib/hecks/runtime/query_interpreter.rb +111 -56
- data/lib/hecks/runtime/reaction_invocation.rb +76 -9
- data/lib/hecks/runtime/read_model_interpreter.rb +40 -40
- data/lib/hecks/runtime/rebuild_sweep.rb +4 -4
- data/lib/hecks/runtime/reference_hop.rb +6 -6
- data/lib/hecks/runtime/refusal_wording.rb +92 -112
- data/lib/hecks/runtime/registry/saga_persistence.rb +21 -21
- data/lib/hecks/runtime/registry/verification.rb +36 -26
- data/lib/hecks/runtime/registry.rb +56 -27
- data/lib/hecks/runtime/remote_dispatcher.rb +38 -23
- data/lib/hecks/runtime/routing.rb +10 -88
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +17 -17
- data/lib/hecks/runtime/saga_interpreter.rb +93 -56
- data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
- data/lib/hecks/runtime/tenant_check.rb +9 -9
- data/lib/hecks/runtime/tenant_scope.rb +5 -5
- data/lib/hecks/runtime/value/admission.rb +75 -30
- data/lib/hecks/runtime/value/coercion.rb +379 -226
- data/lib/hecks/runtime/value/entity_list_coercion.rb +248 -0
- data/lib/hecks/runtime/value.rb +28 -23
- data/lib/hecks/runtime.rb +7 -7
- data/lib/hecks/storehouse.rb +64 -64
- data/lib/hecks/version.rb +3 -3
- data/lib/hecks/vocabulary.rb +207 -5
- data/lib/hecks.rb +13 -11
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +11 -11
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +14 -12
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +11 -11
- metadata +43 -2
data/lib/hecks/naming.rb
CHANGED
|
@@ -5,14 +5,14 @@ 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
|
|
|
@@ -39,7 +39,48 @@ module Hecks
|
|
|
39
39
|
.downcase
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
-
#
|
|
42
|
+
# An identifier as a person would say it — `ATMCard` -> "ATM card",
|
|
43
|
+
# `AccrueInterest` -> "Accrue interest", `daily_limit` -> "Daily
|
|
44
|
+
# limit", `Back office` -> "Back office". The same two word-boundary
|
|
45
|
+
# splits `snake` uses, with a space instead of an underscore — plus
|
|
46
|
+
# the one thing `snake` cannot give back: an all-caps run stays an
|
|
47
|
+
# acronym ("ATM", "KYC") instead of being lowercased into a word
|
|
48
|
+
# nobody says ("Atm"). First word capitalized, the rest lowercased,
|
|
49
|
+
# so a headword reads as sentence case whatever casing it was
|
|
50
|
+
# declared in.
|
|
51
|
+
def words(text)
|
|
52
|
+
parts = text.to_s
|
|
53
|
+
.tr("_", " ")
|
|
54
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1 \2')
|
|
55
|
+
.gsub(/([a-z\d])([A-Z])/, '\1 \2')
|
|
56
|
+
.split
|
|
57
|
+
parts.each_with_index.map do |part, index|
|
|
58
|
+
next part if part.match?(/\A[A-Z]{2,}\z/)
|
|
59
|
+
|
|
60
|
+
index.zero? ? part.capitalize : part.downcase
|
|
61
|
+
end.join(" ")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# "A, B, and C" / "A or B" — the Oxford-comma list every English
|
|
65
|
+
# sentence a projection writes wants; lived in `NarrateProjector`
|
|
66
|
+
# alone until a second projection needed it.
|
|
67
|
+
def to_sentence_list(items, conj: "and")
|
|
68
|
+
case items.size
|
|
69
|
+
when 0 then ""
|
|
70
|
+
when 1 then items[0].to_s
|
|
71
|
+
when 2 then "#{items[0]} #{conj} #{items[1]}"
|
|
72
|
+
else "#{items[0..-2].join(', ')}, #{conj} #{items[-1]}"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# The vowel-letter heuristic — safe here for the same reason
|
|
77
|
+
# `Projections::Statements#article` gives: a construct name is a
|
|
78
|
+
# plain word, never "hour" or "university".
|
|
79
|
+
def a_or_an(word)
|
|
80
|
+
%w[a e i o u].include?(word.to_s[0].to_s.downcase) ? "an" : "a"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# The name a collection of something takes.
|
|
43
84
|
#
|
|
44
85
|
# There were two of these and one was wrong. A read model's gathered heads
|
|
45
86
|
# derived their name with a bare `"#{snake(target)}s"`, so the meta-domain's
|
|
@@ -56,9 +97,9 @@ module Hecks
|
|
|
56
97
|
"#{word}s"
|
|
57
98
|
end
|
|
58
99
|
|
|
59
|
-
# `has_many`'s undo — the plural
|
|
100
|
+
# `has_many`'s undo — the plural written, back to the singular the target
|
|
60
101
|
# aggregate is actually named. Deliberately the crude half of a pair: `plural`
|
|
61
|
-
# above earns its precision (three suffix rules) because getting a
|
|
102
|
+
# above earns its precision (three suffix rules) because getting a collection
|
|
62
103
|
# name wrong reads as a typo forever ; this only ever recovers a name someone
|
|
63
104
|
# already wrote as a real aggregate, so "ies -> y, trailing s dropped" is the
|
|
64
105
|
# whole rule — enough for `has_many Invoices` to resolve to the aggregate
|
|
@@ -67,11 +108,11 @@ module Hecks
|
|
|
67
108
|
word = text.to_s
|
|
68
109
|
return "#{word[0..-4]}y" if word.length > 3 && word.end_with?("ies")
|
|
69
110
|
|
|
70
|
-
# `plural`'s
|
|
111
|
+
# `plural`'s own second rule adds "es" (not bare "s") after
|
|
71
112
|
# s/x/z/ch/sh — undone here the same way, or a word `plural`
|
|
72
113
|
# itself would have suffixed with "es" comes back missing its
|
|
73
114
|
# own trailing letter ("Boxes" -> "Boxe", not "Box") once this
|
|
74
|
-
# only ever knew how to drop a bare "s". Checked
|
|
115
|
+
# only ever knew how to drop a bare "s". Checked before the
|
|
75
116
|
# bare-"s" rule below: stripping "es" first and confirming what
|
|
76
117
|
# is left actually ends in one of those five shapes is what
|
|
77
118
|
# keeps an ordinary "-es" word (e.g. "Invoices" -> "Invoice")
|
|
@@ -102,28 +143,55 @@ module Hecks
|
|
|
102
143
|
text.include?(".") ? text.split(".", 2).last : text
|
|
103
144
|
end
|
|
104
145
|
|
|
146
|
+
# Domain, aggregate, then the REST dot-joined into one command path.
|
|
147
|
+
#
|
|
148
|
+
# The `::` boundary between domain and aggregate is unambiguous by
|
|
149
|
+
# construction — every caller here has already prefixed the domain
|
|
150
|
+
# itself (`PolicyInterpreter#deliver`, `SagaInterpreter#qualified`,
|
|
151
|
+
# `Router#dispatch`'s own rebuilt string) before this ever runs. A
|
|
152
|
+
# third `::` segment can still show up past that boundary: a bare
|
|
153
|
+
# `ScopedConstant` naming a port operation (`command_ref`'s own
|
|
154
|
+
# comment — `Aggregate::Port::Operation`, three colon-joined
|
|
155
|
+
# segments with no `.` of its own) only gets its last `::` rewritten
|
|
156
|
+
# to `.` there, at DSL-build time, because nothing at that point
|
|
157
|
+
# knows yet whether the constant names a port operation or a
|
|
158
|
+
# domain-qualified command (`Domain::Aggregate::Command`, the other
|
|
159
|
+
# shape `command_ref` documents) — both are textually identical.
|
|
160
|
+
# Here, past the already-resolved domain boundary, any leftover
|
|
161
|
+
# `::` is unambiguous: it is that same rewrite artifact, and folding
|
|
162
|
+
# it into the dot-joined tail recovers exactly the
|
|
163
|
+
# `Aggregate::Port.Operation` shape a working port dispatch already
|
|
164
|
+
# expects (`spec/port_operation_interpreter_spec.rb`'s own
|
|
165
|
+
# `"Payments::Payment.PaymentGateway.Receive"`). `Outbox::Fanout
|
|
166
|
+
# .kind_for` and `ReactionInvocation#resolve_target` both already
|
|
167
|
+
# assume this contract on their own end; this is what actually
|
|
168
|
+
# delivers it to them.
|
|
105
169
|
def split_verb(verb)
|
|
106
170
|
path, command = verb.to_s.split(".", 2)
|
|
107
|
-
|
|
108
|
-
|
|
171
|
+
return nil unless path && command
|
|
172
|
+
|
|
173
|
+
domain, aggregate, *rest = path.to_s.split("::")
|
|
174
|
+
return nil unless domain && aggregate
|
|
175
|
+
|
|
176
|
+
command = "#{rest.join('.')}.#{command}" unless rest.empty?
|
|
109
177
|
|
|
110
178
|
[domain, aggregate, command]
|
|
111
179
|
end
|
|
112
180
|
|
|
113
181
|
# `trigger Account::Debit` / `dispatch Account::Debit` — a bare
|
|
114
|
-
#
|
|
182
|
+
# constant reference (`ConstShim`'s own `ScopedConstant`, S0b), not
|
|
115
183
|
# text (ADR 0025, "events and reactions" — command references become
|
|
116
|
-
# first-class). Ruby's `::` joins
|
|
184
|
+
# first-class). Ruby's `::` joins every segment the same way a
|
|
117
185
|
# constant path always does, but a command's own `hecks_fqn` joins
|
|
118
186
|
# its aggregate with `.` (`Construct#hecks_separator`'s default,
|
|
119
|
-
# only an
|
|
187
|
+
# only an aggregate overrides it to `::`) — so only the last `::`
|
|
120
188
|
# becomes a `.`; everything before it (the chapter, when a domain is
|
|
121
189
|
# spelled at all: `Banking::Account::Debit`) stays `::`-joined.
|
|
122
190
|
#
|
|
123
|
-
# A
|
|
191
|
+
# A string passes through unchanged, on purpose — legacy era text
|
|
124
192
|
# (S0a's own shadow-parsed spelling) already mixes `::` (domain) and
|
|
125
193
|
# `.` (command) correctly on its own, e.g. `"Banking::Account.Debit"`,
|
|
126
|
-
# and re-splitting that by content rather than by
|
|
194
|
+
# and re-splitting that by content rather than by type would corrupt
|
|
127
195
|
# it (its own last `::` sits between the domain and the aggregate,
|
|
128
196
|
# not the aggregate and the command). Only an actual constant object
|
|
129
197
|
# — never seen holding a `.` of its own — needs the rewrite at all.
|
|
@@ -148,25 +216,25 @@ module Hecks
|
|
|
148
216
|
|
|
149
217
|
# `transition Account::AccountDebited => "state"` / `starts_on
|
|
150
218
|
# Transfer::TransferRequested` / `ends_on Transfer::TransferSettled`
|
|
151
|
-
# — a process manager's
|
|
152
|
-
#
|
|
219
|
+
# — a process manager's own event references (ADR 0025, S6),
|
|
220
|
+
# deliberately not `event_ref` — found live, not assumed, wiring a
|
|
153
221
|
# real migrated corpus site into `bin/model_check` for the first
|
|
154
222
|
# time (2026-08-28): `SagaInterpreter#begin_saga`/`#advance_saga`
|
|
155
223
|
# match `pm.starts_on`/`pm.handler_for` against `event.name`, which
|
|
156
|
-
# `CommandRules::Emission#emit` stamps
|
|
224
|
+
# `CommandRules::Emission#emit` stamps bare — a command's own
|
|
157
225
|
# `emits AccountDebited` never carries its aggregate's name at all
|
|
158
226
|
# (unlike a policy's cross-aggregate `on`, matched instead by
|
|
159
227
|
# `Naming.demodulise(event.aggregate)` split apart from the bare
|
|
160
228
|
# name — `PolicyInterpreter#policies_for`). Handing a saga's own
|
|
161
|
-
# matcher the
|
|
229
|
+
# matcher the dotted `event_ref` form ("Account.AccountDebited")
|
|
162
230
|
# would silently name an event no command in the domain ever
|
|
163
231
|
# actually emits — caught by `bin/model_check`'s own `deaf_handler`/
|
|
164
232
|
# `deaf_trigger` findings the moment a real qualified corpus site
|
|
165
233
|
# existed to trip them, not by any unit test in isolation.
|
|
166
234
|
#
|
|
167
|
-
# A qualifier is still worth
|
|
235
|
+
# A qualifier is still worth writing (`Account::`) — the same
|
|
168
236
|
# provenance a reader gets from `trigger Account::Debit` — it is
|
|
169
|
-
# only not worth
|
|
237
|
+
# only not worth keeping: `demodulise` drops everything but the
|
|
170
238
|
# final segment, so `Account::AccountDebited` and a bare
|
|
171
239
|
# `AccountDebited` resolve to the identical stored string. A String
|
|
172
240
|
# passes through unchanged either way, exactly like `command_ref`'s
|
|
@@ -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,82 @@ 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, used to resolve the adapter
|
|
32
|
+
# and handed on to it
|
|
33
|
+
# @param identity_id [Object] adapter-defined identity key, forwarded unchanged
|
|
34
|
+
# @return [Object] adapter-defined session representation
|
|
35
|
+
# @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
|
|
36
|
+
# (see `adapter`)
|
|
26
37
|
def session_for_identity(registry, identity_id:)
|
|
27
38
|
adapter(registry).session_for_identity(registry, identity_id: identity_id)
|
|
28
39
|
end
|
|
29
40
|
|
|
41
|
+
# Admits a new person, in whatever way this domain's adapter defines admission.
|
|
42
|
+
#
|
|
43
|
+
# The keywords carry the names of a verified sign-in (`Ports::Authentication.verify`
|
|
44
|
+
# answers `issuer`, `subject` and `email`), but nothing in this repository wires the
|
|
45
|
+
# two together, so their shapes here are adapter-defined.
|
|
46
|
+
#
|
|
47
|
+
# @param registry [Runtime::Registry] the booted registry, used to resolve the adapter
|
|
48
|
+
# and handed on to it
|
|
49
|
+
# @param email [Object] adapter-defined, forwarded unchanged; the person's email
|
|
50
|
+
# @param issuer [Object] adapter-defined, forwarded unchanged; the OIDC issuer that
|
|
51
|
+
# authenticated the person
|
|
52
|
+
# @param subject [Object] adapter-defined, forwarded unchanged; the OIDC subject the
|
|
53
|
+
# issuer vouches for
|
|
54
|
+
# @return [Object] adapter-defined representation of the newly admitted person
|
|
55
|
+
# @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
|
|
56
|
+
# (see `adapter`)
|
|
30
57
|
def provision(registry, email:, issuer:, subject:)
|
|
31
58
|
adapter(registry).provision(registry, email: email, issuer: issuer, subject: subject)
|
|
32
59
|
end
|
|
33
60
|
|
|
61
|
+
# Asks the domain's adapter which roles it can grant.
|
|
62
|
+
#
|
|
63
|
+
# @param registry [Runtime::Registry] the booted registry, used to resolve the adapter
|
|
64
|
+
# and handed on to it
|
|
65
|
+
# @return [Object] adapter-defined collection of grantable roles
|
|
66
|
+
# @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
|
|
67
|
+
# (see `adapter`)
|
|
34
68
|
def available_roles(registry)
|
|
35
69
|
adapter(registry).available_roles(registry)
|
|
36
70
|
end
|
|
37
71
|
|
|
72
|
+
# Grants a role to a person, by whatever means the domain's adapter records a grant.
|
|
73
|
+
#
|
|
74
|
+
# @param registry [Runtime::Registry] the booted registry, used to resolve the adapter
|
|
75
|
+
# and handed on to it
|
|
76
|
+
# @param email [Object] adapter-defined, forwarded unchanged; the person receiving the role
|
|
77
|
+
# @param role [Object] adapter-defined, forwarded unchanged; the role to grant
|
|
78
|
+
# @return [Object] adapter-defined representation of the grant
|
|
79
|
+
# @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
|
|
80
|
+
# (see `adapter`)
|
|
38
81
|
def grant(registry, email:, role:)
|
|
39
82
|
adapter(registry).grant(registry, email: email, role: role)
|
|
40
83
|
end
|
|
41
84
|
|
|
85
|
+
# Lists every person the domain's adapter knows about.
|
|
86
|
+
#
|
|
87
|
+
# @param registry [Runtime::Registry] the booted registry, used to resolve the adapter
|
|
88
|
+
# and handed on to it
|
|
89
|
+
# @return [Object] adapter-defined collection of people
|
|
90
|
+
# @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
|
|
91
|
+
# (see `adapter`)
|
|
42
92
|
def all_people(registry)
|
|
43
93
|
adapter(registry).all_people(registry)
|
|
44
94
|
end
|
|
45
95
|
|
|
96
|
+
# Finds the single adapter bound to this port, refusing an ambiguous wiring.
|
|
97
|
+
#
|
|
98
|
+
# @param registry [Runtime::Registry] the booted registry to search
|
|
99
|
+
# @return [Module] the adapter module or class implementing this port
|
|
100
|
+
# @raise [Runtime::WiringError] if no adapter, or more than one, implements this port,
|
|
101
|
+
# or the one that does has no Ruby implementation under `Hecks::Adapters`
|
|
46
102
|
def adapter(registry)
|
|
47
103
|
implementations = registry.adapters.values.select { |a| a.port == NAME }
|
|
48
104
|
|
|
@@ -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
|