hecks 1.3.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 +5 -4
- 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/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 +18 -18
- data/lib/hecks/behaviors/dsl.rb +2 -2
- data/lib/hecks/behaviors/expectations.rb +23 -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 +7 -7
- 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 +101 -87
- 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 +170 -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 +180 -19
- 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 +140 -45
- 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 +37 -18
- data/lib/hecks/fuzzing/form_census.rb +35 -20
- 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 +62 -62
- data/lib/hecks/fuzzing/nondeterministic.rb +67 -0
- data/lib/hecks/fuzzing/persistence_parity.rb +19 -21
- data/lib/hecks/fuzzing/properties/corrections.rb +9 -9
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +154 -49
- data/lib/hecks/fuzzing/properties/guards.rb +42 -42
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +32 -32
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +22 -35
- data/lib/hecks/fuzzing/properties/outbox.rb +24 -24
- data/lib/hecks/fuzzing/properties/querying.rb +17 -17
- data/lib/hecks/fuzzing/properties.rb +64 -40
- data/lib/hecks/fuzzing/qa_settings.rb +152 -0
- data/lib/hecks/fuzzing/replay.rb +114 -114
- data/lib/hecks/fuzzing/rotation_priority.rb +10 -10
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +113 -0
- data/lib/hecks/fuzzing/self_consistency.rb +141 -112
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +35 -35
- 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 +58 -31
- data/lib/hecks/fuzzing/sequence_generator.rb +111 -23
- data/lib/hecks/fuzzing/shrinker.rb +197 -0
- data/lib/hecks/fuzzing/structural_skips.rb +23 -130
- data/lib/hecks/fuzzing/sweep_depth.rb +4 -4
- data/lib/hecks/fuzzing/target_capabilities.rb +49 -18
- data/lib/hecks/fuzzing/value_generator.rb +14 -14
- data/lib/hecks/fuzzing.rb +5 -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 +348 -13
- data/lib/hecks/language/oidc.json +5 -0
- data/lib/hecks/literal.rb +9 -9
- data/lib/hecks/naming.rb +25 -25
- 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 +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 +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 +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 +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 +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 +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 +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 +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 +3 -3
- data/lib/hecks/projections/glossary/markdown.rb +3 -3
- data/lib/hecks/projections/glossary/mermaid.rb +1 -1
- data/lib/hecks/projections/glossary/sections.rb +1 -1
- data/lib/hecks/projections/glossary/sentences.rb +5 -5
- data/lib/hecks/projections/glossary.rb +15 -15
- 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 +11 -11
- 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 +109 -27
- 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 +42 -63
- 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 +35 -35
- 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 +151 -67
- data/lib/hecks/runtime/entity_interpreter.rb +108 -82
- 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 +55 -64
- data/lib/hecks/runtime/reaction_invocation.rb +20 -20
- 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 -115
- 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 +45 -28
- data/lib/hecks/runtime/remote_dispatcher.rb +38 -23
- data/lib/hecks/runtime/routing.rb +10 -153
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +17 -17
- data/lib/hecks/runtime/saga_interpreter.rb +64 -64
- 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 +318 -127
- data/lib/hecks/runtime/value/entity_list_coercion.rb +34 -34
- data/lib/hecks/runtime/value.rb +21 -21
- 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 +205 -4
- 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 +22 -2
data/lib/hecks/forms/html.rb
CHANGED
|
@@ -11,8 +11,14 @@ module Hecks
|
|
|
11
11
|
# account number a support rep typed into a form and got wrong — flows
|
|
12
12
|
# through here on every render, so escaping is not optional decoration.
|
|
13
13
|
module Escape
|
|
14
|
+
# Escapes a value for HTML text content, replacing `&`, `<`, `>`, `"` and `'` with
|
|
15
|
+
# entities so nothing in it becomes a tag.
|
|
16
|
+
#
|
|
14
17
|
# Order matters — `&` first, or every escape this method itself just
|
|
15
18
|
# wrote (`&`, `<`, ...) gets re-escaped a second time.
|
|
19
|
+
#
|
|
20
|
+
# @param value [Object, nil] the value to show, rendered with `to_s`; nil renders as `""`
|
|
21
|
+
# @return [String] the escaped text
|
|
16
22
|
def self.html(value)
|
|
17
23
|
value.to_s
|
|
18
24
|
.gsub("&", "&")
|
|
@@ -22,14 +28,22 @@ module Hecks
|
|
|
22
28
|
.gsub("'", "'")
|
|
23
29
|
end
|
|
24
30
|
|
|
31
|
+
# Escapes a value for use inside a quoted HTML attribute.
|
|
32
|
+
#
|
|
25
33
|
# Safe inside a double-quoted HTML attribute specifically — `html`
|
|
26
34
|
# already covers this (it escapes `"`), kept as a named alias so a
|
|
27
35
|
# call site reads "this value fills an attribute" rather than repeating
|
|
28
36
|
# the same escaping and leaving the reader to check they match.
|
|
37
|
+
#
|
|
38
|
+
# @param value [Object, nil] the attribute value, rendered with `to_s`; nil renders
|
|
39
|
+
# as `""`
|
|
40
|
+
# @return [String] the escaped text, without surrounding quotes
|
|
29
41
|
def self.attr(value) = html(value)
|
|
30
42
|
|
|
43
|
+
# Percent-encodes a value for use as a query-string value.
|
|
44
|
+
#
|
|
31
45
|
# L12 (docs/audits/2026-08-10-main-bug-audit.md) — safe as a
|
|
32
|
-
# query-string
|
|
46
|
+
# query-string value. `html`/`attr` guard against the value becoming
|
|
33
47
|
# markup, but say nothing about it staying inside the URL syntax
|
|
34
48
|
# position it was placed in: an aggregate's identity is free-form
|
|
35
49
|
# unless its value object declares a `pattern:` (see S3 in the same
|
|
@@ -38,23 +52,32 @@ module Hecks
|
|
|
38
52
|
# interpolation (a stray `&` smuggles a second query parameter, `#`
|
|
39
53
|
# truncates the path at a fragment, `/` splits the path into an
|
|
40
54
|
# extra segment, ...). Percent-encodes via
|
|
41
|
-
# `application/x-www-form-urlencoded` (`+` for space) — correct
|
|
55
|
+
# `application/x-www-form-urlencoded` (`+` for space) — correct only
|
|
42
56
|
# for a query-string value (query_form_renderer.rb's `quick_links`,
|
|
43
|
-
# record_renderer.rb's `?to=`). For a URL
|
|
57
|
+
# record_renderer.rb's `?to=`). For a URL path segment use `path`
|
|
44
58
|
# below instead — `+` is a literal plus there, not an escaped space,
|
|
45
59
|
# so this method would corrupt any id containing a space. Callers
|
|
46
|
-
# still wrap the
|
|
60
|
+
# still wrap the assembled href/Location in `attr` (or `html`) as
|
|
47
61
|
# usual — this only covers the id's own component, not the
|
|
48
62
|
# surrounding markup.
|
|
63
|
+
#
|
|
64
|
+
# @param value [Object, nil] the value to encode, rendered with `to_s`
|
|
65
|
+
# @return [String] the `application/x-www-form-urlencoded` form, a space rendered as `+`
|
|
49
66
|
def self.url(value) = URI.encode_www_form_component(value.to_s)
|
|
50
67
|
|
|
51
|
-
#
|
|
68
|
+
# Percent-encodes a value for use as one URL path segment.
|
|
69
|
+
#
|
|
70
|
+
# Same guard as `url`, for a URL path segment instead of a
|
|
52
71
|
# query-string value. `encode_www_form_component` renders space as
|
|
53
72
|
# `+`, which is only meaningful inside a query string — in a path
|
|
54
73
|
# segment `+` is a literal plus, so an id like "John Smith" would
|
|
55
74
|
# round-trip to "John+Smith" and 404 against the real id "John
|
|
56
75
|
# Smith". Reuse the same percent-encoding and just correct that one
|
|
57
76
|
# character back to `%20`.
|
|
77
|
+
#
|
|
78
|
+
# @param value [Object, nil] the segment to encode, rendered with `to_s`
|
|
79
|
+
# @return [String] the percent-encoded segment, a space rendered as `%20` and `/` as
|
|
80
|
+
# `%2F`
|
|
58
81
|
def self.path(value) = URI.encode_www_form_component(value.to_s).gsub("+", "%20")
|
|
59
82
|
end
|
|
60
83
|
|
|
@@ -64,6 +87,14 @@ module Hecks
|
|
|
64
87
|
# `true` renders as a bare boolean attribute (`required`, not
|
|
65
88
|
# `required="true"`); `nil`/`false` are dropped entirely.
|
|
66
89
|
module Tag
|
|
90
|
+
# Renders a Hash of attributes as the text that sits inside an opening tag, escaping
|
|
91
|
+
# every value and spelling an underscored name with hyphens.
|
|
92
|
+
#
|
|
93
|
+
# @param pairs [Hash{Symbol => Object}] attribute values by name; `true`
|
|
94
|
+
# renders the bare name, nil and `false` drop the attribute, anything else is
|
|
95
|
+
# rendered with `to_s`
|
|
96
|
+
# @return [String] space-separated attributes, such as `required aria-describedby="x"`;
|
|
97
|
+
# `""` when every pair is dropped
|
|
67
98
|
def self.attrs(pairs)
|
|
68
99
|
pairs.filter_map do |name, value|
|
|
69
100
|
next if value.nil? || value == false
|
|
@@ -73,15 +104,28 @@ module Hecks
|
|
|
73
104
|
end.join(" ")
|
|
74
105
|
end
|
|
75
106
|
|
|
107
|
+
# Renders an opening tag with its attributes. The tag name is interpolated unescaped,
|
|
108
|
+
# so it must come from code, never from domain data.
|
|
109
|
+
#
|
|
110
|
+
# @param name [String] the element name, such as `"input"`
|
|
111
|
+
# @param pairs [Hash{Symbol => Object}] attributes, rendered as `attrs` renders them
|
|
112
|
+
# @return [String] the opening tag, such as `<input type="text" required>`
|
|
76
113
|
def self.open(name, **pairs)
|
|
77
114
|
rendered = attrs(pairs)
|
|
78
115
|
rendered.empty? ? "<#{name}>" : "<#{name} #{rendered}>"
|
|
79
116
|
end
|
|
80
117
|
|
|
81
|
-
#
|
|
118
|
+
# Renders a void element such as `<input>`. A self-closing tag reads the same as an
|
|
119
|
+
# opening one; HTML5 needs no slash.
|
|
120
|
+
#
|
|
121
|
+
# `self.open(...)`, not bare `open(...)` — this `Tag.open` (an HTML tag
|
|
82
122
|
# renderer, right above) shadows Kernel#open safely either way, but the
|
|
83
123
|
# explicit receiver also settles Security/Open's static ambiguity.
|
|
84
|
-
|
|
124
|
+
#
|
|
125
|
+
# @param name [String] the element name, such as `"input"`
|
|
126
|
+
# @param pairs [Hash{Symbol => Object}] attributes, rendered as `attrs` renders them
|
|
127
|
+
# @return [String] the tag, identical to what `open` renders
|
|
128
|
+
def self.void(name, **pairs) = self.open(name, **pairs)
|
|
85
129
|
end
|
|
86
130
|
end
|
|
87
131
|
end
|
|
@@ -4,9 +4,15 @@ module Hecks
|
|
|
4
4
|
module Forms
|
|
5
5
|
# The home page: every exposed chapter, every aggregate on it — the
|
|
6
6
|
# entry point into what would otherwise be a URL you'd have to already
|
|
7
|
-
# know. `chapters` is `{domain_name => Bluebook}`, in `expose`
|
|
8
|
-
# (see
|
|
7
|
+
# know. `chapters` is `{domain_name => Bluebook::Chapter}`, in `expose`
|
|
8
|
+
# order (see `Forms::Config` in forms.rb).
|
|
9
9
|
module IndexRenderer
|
|
10
|
+
# Renders the home page body: one section per exposed chapter, each linking to its
|
|
11
|
+
# aggregates.
|
|
12
|
+
#
|
|
13
|
+
# @param chapters [Hash{String => Bluebook::Chapter}] the loaded chapters by domain
|
|
14
|
+
# name, in `expose` order
|
|
15
|
+
# @return [String] the HTML page body; only the heading when `chapters` is empty
|
|
10
16
|
def self.render(chapters)
|
|
11
17
|
sections = chapters.map { |name, chapter| chapter_section(name, chapter) }
|
|
12
18
|
<<~HTML
|
|
@@ -15,6 +21,12 @@ module Hecks
|
|
|
15
21
|
HTML
|
|
16
22
|
end
|
|
17
23
|
|
|
24
|
+
# Renders one chapter's section: its name, its vision as a badge when it declares one,
|
|
25
|
+
# and a link per aggregate showing how many commands and queries it has.
|
|
26
|
+
#
|
|
27
|
+
# @param name [String] the domain name, used as the heading and the first path segment
|
|
28
|
+
# @param chapter [Bluebook::Chapter] the loaded chapter
|
|
29
|
+
# @return [String] HTML for the section's heading and aggregate list
|
|
18
30
|
def self.chapter_section(name, chapter)
|
|
19
31
|
items = chapter.aggregates.map do |aggregate|
|
|
20
32
|
counts = "#{aggregate.commands.size} command#{'s' unless aggregate.commands.size == 1}, " \
|
data/lib/hecks/forms/params.rb
CHANGED
|
@@ -4,35 +4,59 @@ require_relative "field_renderer"
|
|
|
4
4
|
|
|
5
5
|
module Hecks
|
|
6
6
|
module Forms
|
|
7
|
-
# The two directions between a flat, dotted,
|
|
7
|
+
# The two directions between a flat, dotted, all-strings web payload
|
|
8
8
|
# (`{"amount.cents"=>"1050", "amount.currency"=>"USD"}`, whether it came
|
|
9
9
|
# off a POST form body or a GET query string — Rack hands back the same
|
|
10
10
|
# flat shape for both as long as nothing uses `[]` bracket names) and the
|
|
11
11
|
# nested, typed hash `Dispatcher#dispatch`/`#query` actually take
|
|
12
12
|
# (`{amount: {cents: 1050, currency: "USD"}}`).
|
|
13
13
|
#
|
|
14
|
-
# `extract`
|
|
14
|
+
# `extract` needs the Field tree, not just the raw params — a numeric
|
|
15
15
|
# leaf's own runtime check (`Value::Coercion#check_numeric_fields`)
|
|
16
16
|
# requires an actual `Integer`/`Float`, not a String that merely looks
|
|
17
17
|
# like one (`given.is_a?(expected)`, no coercion attempted there); a web
|
|
18
18
|
# form can only ever hand back strings, so the cast has to happen here,
|
|
19
|
-
# once, using the
|
|
19
|
+
# once, using the same shape `FieldShape` already resolved for
|
|
20
20
|
# rendering the input in the first place — one reading of the IR, not
|
|
21
21
|
# two that could disagree.
|
|
22
22
|
module Params
|
|
23
|
-
#
|
|
23
|
+
# Casts a flat, all-strings web payload into the nested, typed arguments a command or
|
|
24
|
+
# query takes, guided by the field tree the form was rendered from.
|
|
25
|
+
#
|
|
26
|
+
# Every leaf field carries its own full dotted path regardless of how
|
|
24
27
|
# deep `FieldShape` nested it to get there — a single-attribute value
|
|
25
|
-
# object unwraps to a leaf sitting at the
|
|
28
|
+
# object unwraps to a leaf sitting at the top of the fields array with
|
|
26
29
|
# a two-segment path (`"reference.value"`), the exact same shape a
|
|
27
30
|
# `:group`'s own child carries. So this collects every leaf as
|
|
28
|
-
# (full path -> value)
|
|
31
|
+
# (full path -> value) first, and nests by the path's own segments
|
|
29
32
|
# last — one nesting rule, blind to how a field arrived at its path.
|
|
33
|
+
#
|
|
34
|
+
# @param fields [Array<Forms::Field>] the resolved field tree the payload was posted
|
|
35
|
+
# against
|
|
36
|
+
# @param raw [Hash{String => String}] the flat web payload, keyed by dotted field path
|
|
37
|
+
# @return [Hash{Symbol => Object}] nested, typed arguments keyed by path segment, ready
|
|
38
|
+
# to splat into a dispatch; a blank optional field is left out
|
|
39
|
+
# @raise [ArgumentError] if a number field's text is not numeric, or two fields collide
|
|
40
|
+
# at one path (see `nest`)
|
|
41
|
+
# @raise [TypeError] if a required number field is missing from `raw`
|
|
42
|
+
# @raise [JSON::ParserError] if a line of a list-of-value-objects field is not JSON
|
|
30
43
|
def self.extract(fields, raw)
|
|
31
44
|
pairs = {}
|
|
32
45
|
fields.each { |field| collect(field, raw, pairs) }
|
|
33
46
|
nest(pairs)
|
|
34
47
|
end
|
|
35
48
|
|
|
49
|
+
# Gathers one field's typed value, and recursively its children's, into `pairs` under
|
|
50
|
+
# each leaf's full dotted path; a blank optional leaf adds nothing.
|
|
51
|
+
#
|
|
52
|
+
# @param field [Forms::Field] the field to read
|
|
53
|
+
# @param raw [Hash{String => String}] the flat web payload, keyed by dotted field path
|
|
54
|
+
# @param pairs [Hash{String => Object}] accumulator of typed values by dotted path,
|
|
55
|
+
# added to in place
|
|
56
|
+
# @return [void]
|
|
57
|
+
# @raise [ArgumentError] if a number field's text is not numeric
|
|
58
|
+
# @raise [TypeError] if a required number field is missing from `raw`
|
|
59
|
+
# @raise [JSON::ParserError] if a line of a list-of-value-objects field is not JSON
|
|
36
60
|
def self.collect(field, raw, pairs)
|
|
37
61
|
case field.kind
|
|
38
62
|
when :group, :money then field.children.each { |child| collect(child, raw, pairs) }
|
|
@@ -41,23 +65,32 @@ module Hecks
|
|
|
41
65
|
end
|
|
42
66
|
end
|
|
43
67
|
|
|
68
|
+
# Nests values keyed by dotted path into a Hash keyed by path segment, refusing two
|
|
69
|
+
# paths that cannot share one result.
|
|
70
|
+
#
|
|
44
71
|
# A path-prefix collision: one field named (say) "price" alongside
|
|
45
|
-
# another named "price.cents" implies "price" is
|
|
72
|
+
# another named "price.cents" implies "price" is both a scalar leaf
|
|
46
73
|
# and the parent of a nested group — the two can never coexist in
|
|
47
74
|
# the same result hash. Depending on which pair `each_with_object`
|
|
48
|
-
# reaches first,
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
#
|
|
75
|
+
# reaches first, an unguarded walk fails in one of two ways: a scalar
|
|
76
|
+
# planted first leaves `acc[segment] ||= {}` seeing a truthy non-Hash
|
|
77
|
+
# and reusing it as `node`, so the next `node[leaf] = value` blows up
|
|
78
|
+
# with a raw `TypeError` from calling `String#[]=` with a Symbol key;
|
|
79
|
+
# a scalar planted after the nested group instead sails through
|
|
80
|
+
# `node[leaf] = value` and silently clobbers the entire nested hash
|
|
81
|
+
# with the scalar, losing every sibling under it with no error at
|
|
82
|
+
# all. Both directions are checked explicitly here so either order
|
|
83
|
+
# raises the same clear `ArgumentError` instead of a confusing crash
|
|
84
|
+
# or silent data loss — this is the family of error every
|
|
85
|
+
# command/query submission path in app.rb already rescues into a 422
|
|
86
|
+
# (`ArgumentError` sits right alongside the domain refusals in every
|
|
87
|
+
# one of those rescue clauses).
|
|
88
|
+
#
|
|
89
|
+
# @param pairs [Hash{String => Object}] values keyed by dotted path, such as
|
|
90
|
+
# `{"amount.cents" => 1050}`
|
|
91
|
+
# @return [Hash{Symbol => Object}] the nested result, such as `{amount: {cents: 1050}}`
|
|
92
|
+
# @raise [ArgumentError] if one path names a plain value where another names a nested
|
|
93
|
+
# group, in either order
|
|
61
94
|
def self.nest(pairs)
|
|
62
95
|
pairs.each_with_object({}) do |(path, value), result|
|
|
63
96
|
segments = path.to_s.split(".").map(&:to_sym)
|
|
@@ -74,6 +107,10 @@ module Hecks
|
|
|
74
107
|
end
|
|
75
108
|
end
|
|
76
109
|
|
|
110
|
+
# Builds, without raising, the error `nest` raises for a path-prefix collision.
|
|
111
|
+
#
|
|
112
|
+
# @param path [String] the dotted path at which the collision was found
|
|
113
|
+
# @return [ArgumentError] an error whose message names the conflicting path
|
|
77
114
|
def self.nesting_collision(path)
|
|
78
115
|
ArgumentError.new("#{path.inspect} conflicts with another field at the same path — " \
|
|
79
116
|
"one names it as a plain value and another as a nested group")
|
|
@@ -82,11 +119,21 @@ module Hecks
|
|
|
82
119
|
SKIP = Object.new.freeze
|
|
83
120
|
private_constant :SKIP
|
|
84
121
|
|
|
122
|
+
# Reads a `:list` field's textarea into an Array, one element per non-blank line.
|
|
123
|
+
#
|
|
85
124
|
# One line of the textarea per element. A line that itself needs
|
|
86
125
|
# several fields (a multi-attribute value object as a list element)
|
|
87
126
|
# is read as JSON on that one line — the honest fallback documented in
|
|
88
127
|
# docs/command-form-and-query-form-bluebook.md rather than a second
|
|
89
128
|
# widget this prototype doesn't build yet.
|
|
129
|
+
#
|
|
130
|
+
# @param field [Forms::Field] a `:list` field whose first child describes one element
|
|
131
|
+
# @param raw [Hash{String => String}] the flat web payload, keyed by dotted field path
|
|
132
|
+
# @return [Array<Object>, Object] the elements — cast scalars for a leaf element,
|
|
133
|
+
# symbol-keyed parsed JSON otherwise — or the private `SKIP` sentinel when the field
|
|
134
|
+
# is absent or blank
|
|
135
|
+
# @raise [ArgumentError] if a number element's line is not numeric
|
|
136
|
+
# @raise [JSON::ParserError] if a non-leaf element's line is not JSON
|
|
90
137
|
def self.extract_list(field, raw)
|
|
91
138
|
text = raw[field.path]
|
|
92
139
|
return SKIP if text.nil? || text.strip.empty?
|
|
@@ -97,6 +144,16 @@ module Hecks
|
|
|
97
144
|
end
|
|
98
145
|
end
|
|
99
146
|
|
|
147
|
+
# Reads one leaf field's typed value out of the payload; a boolean always answers, since
|
|
148
|
+
# an unticked checkbox is itself a value.
|
|
149
|
+
#
|
|
150
|
+
# @param field [Forms::Field] a leaf field
|
|
151
|
+
# @param raw [Hash{String => String}] the flat web payload, keyed by dotted field path
|
|
152
|
+
# @return [Boolean, Integer, Float, String, nil, Object] the cast value; nil when a
|
|
153
|
+
# required non-number field is missing; the private `SKIP` sentinel when an optional
|
|
154
|
+
# field is absent or empty
|
|
155
|
+
# @raise [ArgumentError] if a number field's text is not numeric
|
|
156
|
+
# @raise [TypeError] if a required number field is missing from `raw`
|
|
100
157
|
def self.extract_leaf(field, raw)
|
|
101
158
|
return checkbox(raw[field.path]) if field.kind == :boolean
|
|
102
159
|
|
|
@@ -106,10 +163,24 @@ module Hecks
|
|
|
106
163
|
cast_scalar(field, text)
|
|
107
164
|
end
|
|
108
165
|
|
|
166
|
+
# Reads a submitted checkbox value as a Boolean.
|
|
167
|
+
#
|
|
168
|
+
# @param raw_value [String, nil] the submitted text; nil when the field was not posted
|
|
169
|
+
# @return [Boolean] true for `"on"`, `"1"` or `"true"` in any letter case, false for
|
|
170
|
+
# anything else
|
|
109
171
|
def self.checkbox(raw_value)
|
|
110
172
|
%w[on 1 true].include?(raw_value.to_s.downcase)
|
|
111
173
|
end
|
|
112
174
|
|
|
175
|
+
# Casts a leaf's submitted text to the Ruby type the runtime's numeric check requires,
|
|
176
|
+
# which accepts an actual `Integer` or `Float` and never a numeric-looking String.
|
|
177
|
+
#
|
|
178
|
+
# @param field [Forms::Field] the leaf the text was submitted for
|
|
179
|
+
# @param text [String, nil] the submitted text
|
|
180
|
+
# @return [Integer, Float, String, nil] an Integer for a `:number` field with `step`
|
|
181
|
+
# `"1"`, a Float for any other `:number` field, and `text` itself for every other kind
|
|
182
|
+
# @raise [ArgumentError] if a number field's text is not numeric
|
|
183
|
+
# @raise [TypeError] if a number field's text is nil
|
|
113
184
|
def self.cast_scalar(field, text)
|
|
114
185
|
case field.kind
|
|
115
186
|
when :number then field.step == "1" ? Integer(text) : Float(text)
|
|
@@ -117,13 +188,23 @@ module Hecks
|
|
|
117
188
|
end
|
|
118
189
|
end
|
|
119
190
|
|
|
191
|
+
# Flattens held values back into the dotted-path, all-strings pairs a query string
|
|
192
|
+
# carries — the reverse of `extract`.
|
|
193
|
+
#
|
|
120
194
|
# The other direction — a nested value hash back to the flat dotted
|
|
121
195
|
# pairs a GET link's query string carries, so a query view's
|
|
122
|
-
# "shareable link" and its filter
|
|
123
|
-
#
|
|
196
|
+
# "shareable link" and its filter form stay two renderings of the
|
|
197
|
+
# same data rather than two formats that can drift. Reads with
|
|
124
198
|
# `FieldRenderer.dig` — the identical full-path lookup a re-rendered
|
|
125
199
|
# input's own value comes from — for the same reason `extract` above
|
|
126
200
|
# nests by full path rather than by tree shape.
|
|
201
|
+
#
|
|
202
|
+
# @param fields [Array<Forms::Field>] the field tree naming which paths to read
|
|
203
|
+
# @param values [Hash{String, Symbol => Object}] held values, flat or nested; see
|
|
204
|
+
# `FieldRenderer.dig`
|
|
205
|
+
# @param into [Hash{String => String}] accumulator the pairs are added to in place
|
|
206
|
+
# @return [Hash{String => String}] `into`, holding one pair per leaf with a non-nil value
|
|
207
|
+
# and one per list (its elements newline-joined, `""` when it holds nothing)
|
|
127
208
|
def self.flatten(fields, values, into: {})
|
|
128
209
|
fields.each do |field|
|
|
129
210
|
case field.kind
|
|
@@ -137,11 +218,19 @@ module Hecks
|
|
|
137
218
|
into
|
|
138
219
|
end
|
|
139
220
|
|
|
221
|
+
# Lists every dotted path a field tree submits under, whether or not anything was
|
|
222
|
+
# entered for it.
|
|
223
|
+
#
|
|
140
224
|
# Every leaf/list path a field tree carries, independent of any
|
|
141
225
|
# values — what `command_form_renderer.rb`'s inspect panel wants
|
|
142
226
|
# ("which fields does this command take"), where `flatten` above wants
|
|
143
|
-
# "what does
|
|
227
|
+
# "what does this one submission look like" and returns nothing for a
|
|
144
228
|
# field nothing was entered for.
|
|
229
|
+
#
|
|
230
|
+
# @param fields [Array<Forms::Field>] the field tree to walk
|
|
231
|
+
# @param into [Array<String>] accumulator the paths are appended to in place
|
|
232
|
+
# @return [Array<String>] `into`, holding each leaf and list path in render order;
|
|
233
|
+
# `:group` and `:money` fields contribute their children's paths, not their own
|
|
145
234
|
def self.paths(fields, into: [])
|
|
146
235
|
fields.each do |field|
|
|
147
236
|
case field.kind
|
|
@@ -152,6 +241,14 @@ module Hecks
|
|
|
152
241
|
into
|
|
153
242
|
end
|
|
154
243
|
|
|
244
|
+
# Renders held values as a URL query string, percent-encoding each flattened path and
|
|
245
|
+
# value.
|
|
246
|
+
#
|
|
247
|
+
# @param fields [Array<Forms::Field>] the field tree naming which paths to read
|
|
248
|
+
# @param values [Hash{String, Symbol => Object}] held values, flat or nested; see
|
|
249
|
+
# `FieldRenderer.dig`
|
|
250
|
+
# @return [String] `key=value` pairs joined with `&`, without a leading `?`; `""` when
|
|
251
|
+
# `flatten` yields no pairs
|
|
155
252
|
def self.to_query_string(fields, values)
|
|
156
253
|
pairs = flatten(fields, values)
|
|
157
254
|
pairs.map { |key, value| "#{URI.encode_www_form_component(key)}=#{URI.encode_www_form_component(value)}" }.join("&")
|
|
@@ -5,13 +5,13 @@ module Hecks
|
|
|
5
5
|
# boot. Two spellings the previous inline version got wrong:
|
|
6
6
|
#
|
|
7
7
|
# --port=8080 the equals form — the old `ARGV.each_cons(2)` scan
|
|
8
|
-
# only ever recognized "--port", "8080" as
|
|
8
|
+
# only ever recognized "--port", "8080" as two
|
|
9
9
|
# separate argv entries, so this spelling matched
|
|
10
10
|
# nothing and silently fell through to the default.
|
|
11
11
|
# -p abc a non-numeric value — the old code did `.to_i` on
|
|
12
12
|
# whatever followed unconditionally, so a typo
|
|
13
13
|
# quietly became port 0 (Rackup/WEBrick's actual
|
|
14
|
-
# behavior for `Port: 0` is to bind an
|
|
14
|
+
# behavior for `Port: 0` is to bind an ephemeral
|
|
15
15
|
# port — arguably useful on purpose elsewhere, but
|
|
16
16
|
# never what a mistyped `-p abc` meant to ask for).
|
|
17
17
|
#
|
|
@@ -16,7 +16,7 @@ module Hecks
|
|
|
16
16
|
# hiding that behind a form); a row of ready-made links for any
|
|
17
17
|
# closed-set parameter, since a caller filtering by an enum should not
|
|
18
18
|
# have to fill in a form to get a link they could just click; a filter
|
|
19
|
-
#
|
|
19
|
+
# form for everything else, which — being a GET form — produces
|
|
20
20
|
# exactly the same kind of link on submit; and the results table once
|
|
21
21
|
# a request actually supplies parameters.
|
|
22
22
|
module QueryFormRenderer
|
|
@@ -60,7 +60,7 @@ module Hecks
|
|
|
60
60
|
|
|
61
61
|
# The one enum-shaped parameter (if there is one) rendered as literal,
|
|
62
62
|
# clickable links — no form to fill in for "show me the suspended
|
|
63
|
-
# ones". Capped at the
|
|
63
|
+
# ones". Capped at the first closed-set field on purpose: a second one
|
|
64
64
|
# would mean a cross product of links, which reads as noise rather
|
|
65
65
|
# than help. A query with more than one is still fully reachable
|
|
66
66
|
# through the filter form below.
|
|
@@ -68,7 +68,7 @@ module Hecks
|
|
|
68
68
|
end
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
-
# Every non-creating command,
|
|
71
|
+
# Every non-creating command, except a lifecycle transition that does
|
|
72
72
|
# not apply from the record's current state — the same rule
|
|
73
73
|
# `Rules#admissible_transition` enforces at dispatch, read here so a
|
|
74
74
|
# link that would only refuse is never offered in the first place.
|
|
@@ -78,7 +78,7 @@ module Hecks
|
|
|
78
78
|
|
|
79
79
|
items = commands.map do |cmd|
|
|
80
80
|
# L12 — the id is free-form (S3): percent-encoded as the query
|
|
81
|
-
#
|
|
81
|
+
# value (a raw `&` here would smuggle a second bogus query
|
|
82
82
|
# parameter), then the assembled href is attribute-escaped as
|
|
83
83
|
# usual.
|
|
84
84
|
href = "/#{domain}/#{aggregate.hecks_name}/#{cmd.hecks_name}.html?to=#{Escape.url(id)}"
|
|
@@ -5,7 +5,7 @@ module Hecks
|
|
|
5
5
|
module Forms
|
|
6
6
|
# The one shape every renderer in this directory reads a record as —
|
|
7
7
|
# `id` plus its state hash. `Runtime::Instance` already answers both,
|
|
8
|
-
# so `repository.all`'s own records pass straight through; a
|
|
8
|
+
# so `repository.all`'s own records pass straight through; a query's
|
|
9
9
|
# answer does not (`QueryInterpreter#call` flattens `{id:}.merge(state)`
|
|
10
10
|
# into one hash with no method to call — see
|
|
11
11
|
# docs/command-form-and-query-form-bluebook.md's note on why), so
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
module Hecks
|
|
2
2
|
module Forms
|
|
3
|
-
#
|
|
3
|
+
# The value-object classification every consumer of a resolved VO
|
|
4
4
|
# shape needs, spelled once — `field_shape.rb` (a command's own
|
|
5
5
|
# form), `ui_schema.rb` (embryonaut_console's table/detail-panel
|
|
6
6
|
# renderer, a separate app, not this repo), and
|
|
7
7
|
# `adapters/driven/sql_query_builder.rb` (a query's own ORDER BY/
|
|
8
|
-
#
|
|
8
|
+
# where compiler) each grew their own copy of "is this VO money-
|
|
9
9
|
# shaped," "does it have exactly one attribute, worth unwrapping,"
|
|
10
10
|
# and "which member of it is numeric" — the same three questions,
|
|
11
11
|
# answered identically by definition (a VO's shape doesn't change
|
|
@@ -22,7 +22,7 @@ module Hecks
|
|
|
22
22
|
value_object.attributes.map { |a| a.name.to_s }.sort == %w[cents currency]
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
# A VO with exactly one attribute is a
|
|
25
|
+
# A VO with exactly one attribute is a name for a scalar, not a
|
|
26
26
|
# genuine group (EmailAddress{address}, CustomerNumber{value}) —
|
|
27
27
|
# [[feedback_name_the_scalar_field]]'s own reasoning, shared here
|
|
28
28
|
# rather than re-decided per caller. Returns the sole attribute,
|
data/lib/hecks/forms.rb
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# `query_form.bluebook` will eventually become — see docs/
|
|
3
3
|
# command-form-and-query-form-bluebook.md), the IR->HTML renderers, and
|
|
4
4
|
# the Rack app that content-negotiates between them and a plain JSON
|
|
5
|
-
# reading of the same dispatch.
|
|
5
|
+
# reading of the same dispatch. Not required by `require "hecks"`
|
|
6
6
|
# itself — a project that never boots this file never pays for `rack`,
|
|
7
7
|
# the same lazy-dependency discipline the Gemfile's own comment already
|
|
8
8
|
# holds `pg`/`oauth2`/`aws-sdk-lambda` to.
|
|
@@ -19,7 +19,7 @@ module Hecks
|
|
|
19
19
|
# `expose` deliberately does and does not cover.
|
|
20
20
|
module Forms
|
|
21
21
|
# `expose`'s own declaration — "which chapters does this app expose" —
|
|
22
|
-
# kept
|
|
22
|
+
# kept outside the `Hecks.*` collector convention (`Hecks.bluebook`,
|
|
23
23
|
# `Hecks.hecksagon`, ...) and outside `Runtime::Registry` entirely, on
|
|
24
24
|
# purpose: a real language word goes through `syntax.bluebook` and
|
|
25
25
|
# `MetaValidator` (see docs/implemented/guides/extending-hecks.md, "a new word is a
|
|
@@ -30,29 +30,49 @@ module Hecks
|
|
|
30
30
|
# see docs/command-form-and-query-form-bluebook.md, "why this isn't
|
|
31
31
|
# syntax.bluebook yet".
|
|
32
32
|
#
|
|
33
|
-
#
|
|
33
|
+
# One `expose` grants a whole chapter, not a command or a query
|
|
34
34
|
# individually — the future `command_form.bluebook`/`query_form.bluebook`
|
|
35
|
-
# words are
|
|
35
|
+
# words are per-declaration (one command, one query, its own form/view,
|
|
36
36
|
# possibly its own overrides), which `expose` doesn't do today and was
|
|
37
37
|
# never trying to; it's the coarse "turn this chapter's forms on at
|
|
38
38
|
# all" switch those finer words will eventually sit inside.
|
|
39
39
|
class Config
|
|
40
40
|
attr_reader :name, :exposes
|
|
41
41
|
|
|
42
|
+
# @param name [String, Symbol] the app name this configuration is registered under;
|
|
43
|
+
# stored as a String
|
|
42
44
|
def initialize(name)
|
|
43
45
|
@name = name.to_s
|
|
44
46
|
@exposes = []
|
|
45
47
|
end
|
|
46
48
|
|
|
49
|
+
# Switches on the forms of one whole chapter for this app; called inside the
|
|
50
|
+
# `Forms.configure` block.
|
|
51
|
+
#
|
|
52
|
+
# @param chapter_name [String, Symbol] name of the chapter (domain) to expose, such as
|
|
53
|
+
# `"Banking"`; stored as a String
|
|
54
|
+
# @return [Array<String>] every chapter name exposed so far, in declaration order
|
|
47
55
|
def expose(chapter_name) = @exposes << chapter_name.to_s
|
|
48
56
|
end
|
|
49
57
|
|
|
58
|
+
# Declares an app and the chapters it exposes, replacing any earlier configuration
|
|
59
|
+
# registered under the same name.
|
|
60
|
+
#
|
|
61
|
+
# @param name [String, Symbol] the app name `App.for` later looks the configuration up by
|
|
62
|
+
# @yield evaluated with `instance_eval` against the new `Forms::Config`, so a bare
|
|
63
|
+
# `expose "Banking"` inside the block reaches `Config#expose`; optional
|
|
64
|
+
# @return [Forms::Config] the configuration just registered
|
|
50
65
|
def self.configure(name, &block)
|
|
51
66
|
config = Config.new(name)
|
|
52
67
|
config.instance_eval(&block) if block
|
|
53
68
|
(@configs ||= {})[config.name] = config
|
|
54
69
|
end
|
|
55
70
|
|
|
71
|
+
# Looks up the configuration an earlier `Forms.configure` registered.
|
|
72
|
+
#
|
|
73
|
+
# @param name [String, Symbol] the app name given to `Forms.configure`
|
|
74
|
+
# @return [Forms::Config, nil] the app's configuration, or nil when no app of that name
|
|
75
|
+
# has been configured
|
|
56
76
|
def self.config(name) = (@configs || {})[name.to_s]
|
|
57
77
|
end
|
|
58
78
|
end
|
data/lib/hecks/fqn.rb
CHANGED
|
@@ -16,7 +16,7 @@ module Hecks
|
|
|
16
16
|
new(realm: realm, domain: domain, version: version, aggregate: aggregate, verb: query, kind: :query)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
#
|
|
19
|
+
# One order-dependent parse pipeline: split -> shape-validate -> dispatch
|
|
20
20
|
# on segment count -> split domain/version -> classify kind -> cross-
|
|
21
21
|
# field validate -> construct. Each step consumes locals (segments, verb,
|
|
22
22
|
# kind) the step before it derived; splitting would mean threading all of
|
|
@@ -2,6 +2,15 @@ Hecks.bluebook "Governance" do
|
|
|
2
2
|
vision "Organizational authority is explicit, reviewable, and historically explainable."
|
|
3
3
|
supporting
|
|
4
4
|
|
|
5
|
+
# WHAT THIS CHAPTER ANSWERS FOR EVERY DOMAIN THAT ATTACHES IT. The role
|
|
6
|
+
# check at dispatch, boot's refusal of an ungoverned `role`, the
|
|
7
|
+
# authorization adapter and the fuzzer's grant steering all read these
|
|
8
|
+
# three verbs from here, rather than recognising "Governance" by name.
|
|
9
|
+
provides "authorization",
|
|
10
|
+
assignments: "RoleAssignment.AssignmentsForActor",
|
|
11
|
+
grant: "RoleAssignment.Assign",
|
|
12
|
+
transitions: "RoleTransition.Allowed"
|
|
13
|
+
|
|
5
14
|
aggregate "RoleAssignment" do
|
|
6
15
|
description "One actor holding one role, over one scope, from one point in time — revoked by setting when it ended, never deleted."
|
|
7
16
|
|