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
|
@@ -9,17 +9,27 @@ module Hecks
|
|
|
9
9
|
# singleton, one aggregate door per declared head, and the
|
|
10
10
|
# declaration hook for names the door does not carry.
|
|
11
11
|
module Chapter
|
|
12
|
+
# Builds the anonymous module that stands for one booted chapter: `vision`,
|
|
13
|
+
# `aggregates`, `docs`, `narrate` and `project` as singleton methods, plus one
|
|
14
|
+
# nested constant per aggregate holding that aggregate's door.
|
|
15
|
+
#
|
|
16
|
+
# @param dispatcher [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted
|
|
17
|
+
# dispatcher each aggregate door closes over
|
|
18
|
+
# @param bluebook [Bluebook::Chapter] the chapter to project into a module
|
|
19
|
+
# @return [Module] a fresh, unnamed module; `Surface.install` gives it its
|
|
20
|
+
# top-level name
|
|
21
|
+
# @raise [NameError] if an aggregate's name is not a valid constant name
|
|
12
22
|
def chapter_module(dispatcher, bluebook)
|
|
13
23
|
chapter = Module.new
|
|
14
24
|
chapter.define_singleton_method(:vision) { bluebook.vision }
|
|
15
25
|
chapter.define_singleton_method(:aggregates) { bluebook.aggregates.map(&:name).sort }
|
|
16
26
|
|
|
17
|
-
#
|
|
27
|
+
# **The chapter, explaining itself**. `Projector::DocsProjector` reads
|
|
18
28
|
# nothing but this bluebook's own IR, so the document cannot drift
|
|
19
29
|
# from the domain — the same guarantee `bin/reference` gives the DSL
|
|
20
30
|
# reference by generating it from the Syntax chapter.
|
|
21
31
|
#
|
|
22
|
-
# A
|
|
32
|
+
# A method rather than only a script because that is what gets it
|
|
23
33
|
# read: `QualityControl.docs` in a console, beside `vision` and
|
|
24
34
|
# `aggregates`, at the moment somebody is wondering what a verb
|
|
25
35
|
# wants. Projected on each call rather than memoised — it is a pure
|
|
@@ -29,14 +39,14 @@ module Hecks
|
|
|
29
39
|
Projector.call(:docs, bluebook: bluebook, options: options)
|
|
30
40
|
end
|
|
31
41
|
|
|
32
|
-
#
|
|
42
|
+
# **The chapter, read back in english** — `Projector::NarrateProjector`
|
|
33
43
|
# beside `:docs`, for the reader who needs to confirm the domain is
|
|
34
44
|
# right rather than call it: `QualityControl.narrate` in a console.
|
|
35
45
|
chapter.define_singleton_method(:narrate) do |**options|
|
|
36
46
|
Projector.call(:narrate, bluebook: bluebook, options: options)
|
|
37
47
|
end
|
|
38
48
|
|
|
39
|
-
#
|
|
49
|
+
# **The domain's own IR, projected**. `Projector` has taken
|
|
40
50
|
# `call(name, bluebook:, options:)` since §30, but nothing could
|
|
41
51
|
# reach it from a booted domain — this module already closes
|
|
42
52
|
# over the one `Bluebook` every projector wants and simply
|
|
@@ -69,26 +79,25 @@ module Hecks
|
|
|
69
79
|
chapter.const_set(aggregate.hecks_name, aggregate_module(dispatcher, bluebook.name, aggregate))
|
|
70
80
|
end
|
|
71
81
|
|
|
72
|
-
#
|
|
73
|
-
# `.hecksagon` may name an aggregate this door does not carry — a
|
|
74
|
-
# door from an earlier boot resolving another registry's chapter,
|
|
75
|
-
#
|
|
76
|
-
# load. With a collector open, the name becomes a `BindingProxy`
|
|
82
|
+
# Inside a hecksagon, a name is a declaration, not a lookup. A
|
|
83
|
+
# `.hecksagon` may name an aggregate this door does not carry — a stale
|
|
84
|
+
# door from an earlier boot resolving another registry's chapter, which
|
|
85
|
+
# can happen because the facade is installed once a boot finishes, not
|
|
86
|
+
# on every load. With a collector open, the name becomes a `BindingProxy`
|
|
77
87
|
# recording the same bind the aggregate module would ; without one, it
|
|
78
|
-
# is a genuine NameError
|
|
88
|
+
# is a genuine NameError.
|
|
79
89
|
#
|
|
80
|
-
#
|
|
90
|
+
# The other reader of a miss is S0b's own bridge (docs/dsl-work-
|
|
81
91
|
# slices.md, const_shim.rb's `ScopedConstant`): a bluebook still being
|
|
82
|
-
#
|
|
83
|
-
# command reference, an `admits:` — reaches
|
|
92
|
+
# declared that names `#{bluebook.name}::Something` — an event, a
|
|
93
|
+
# command reference, an `admits:` — reaches here, not
|
|
84
94
|
# `Object.const_missing`, the moment this chapter's own facade already
|
|
85
95
|
# exists (a previous boot in the same process, or this same chapter
|
|
86
96
|
# re-entering its own name). `ConstShim.active?` is exactly as true
|
|
87
97
|
# here as it is at the top level, mid-declaration, and consulting the
|
|
88
|
-
#
|
|
89
|
-
# whether or not a facade happens to be built yet —
|
|
90
|
-
#
|
|
91
|
-
# that worked only BEFORE any facade existed.
|
|
98
|
+
# same resolver is what makes a scoped reference resolve identically
|
|
99
|
+
# whether or not a facade happens to be built yet — a bridge consulted
|
|
100
|
+
# only at the top level works only before any facade exists.
|
|
92
101
|
chapter.define_singleton_method(:const_missing) do |name|
|
|
93
102
|
collector = Bluebook::DSL::HecksagonBuilder.collector
|
|
94
103
|
return Bluebook::DSL::BindingProxy.new("#{bluebook.name}::#{name}", collector) if collector
|
data/lib/hecks/facade/surface.rb
CHANGED
|
@@ -3,9 +3,9 @@ require_relative "surface/aggregate_door"
|
|
|
3
3
|
|
|
4
4
|
module Hecks
|
|
5
5
|
module Facade
|
|
6
|
-
#
|
|
6
|
+
# **The door, without the classes**.
|
|
7
7
|
#
|
|
8
|
-
# `Pizzas::Pizza.create_pizza(...)` is the public surface (
|
|
8
|
+
# `Pizzas::Pizza.create_pizza!(...)` is the public surface (handover rule 3),
|
|
9
9
|
# and this is what serves it now : anonymous per-boot modules whose
|
|
10
10
|
# singleton methods close over the dispatcher and dispatch by FQN — the
|
|
11
11
|
# same shape `Router::NamespaceInstaller` proved. Nothing here is a domain
|
|
@@ -17,7 +17,7 @@ module Hecks
|
|
|
17
17
|
# The hexagon-binding door rides along : `Pizzas::Pizza.persisted_by("Heki")`
|
|
18
18
|
# in a `.hecksagon` file lands on the module's `method_missing`, which
|
|
19
19
|
# records an `Bind` into whatever `HecksagonBuilder.collector` is open
|
|
20
|
-
#
|
|
20
|
+
# at call time — so even a facade left over from a previous boot records
|
|
21
21
|
# into the current builder, and a chapter with no constant at all falls
|
|
22
22
|
# through to `ConstShim` → `BindingProxy`, which mints byte-identical binds.
|
|
23
23
|
module Surface
|
|
@@ -30,6 +30,19 @@ module Hecks
|
|
|
30
30
|
|
|
31
31
|
module_function
|
|
32
32
|
|
|
33
|
+
# Installs one top-level module per booted chapter, and one per aggregate, each
|
|
34
|
+
# closing over `dispatcher`, so `Pizzas::Pizza` and the bare `Pizza` both open the
|
|
35
|
+
# door of the boot that ran last.
|
|
36
|
+
#
|
|
37
|
+
# An aggregate sharing its chapter's name gets no second constant: the chapter
|
|
38
|
+
# module already holds that name. A name that user code or the stdlib already owns
|
|
39
|
+
# is left alone with a warning (see `Namespace.install`).
|
|
40
|
+
#
|
|
41
|
+
# @param dispatcher [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted
|
|
42
|
+
# dispatcher every installed door dispatches and queries through
|
|
43
|
+
# @return [Runtime::Dispatcher, Runtime::RemoteDispatcher] the same `dispatcher`,
|
|
44
|
+
# so a boot can return the call's result
|
|
45
|
+
# @raise [NameError] if a chapter or aggregate name is not a valid constant name
|
|
33
46
|
def install(dispatcher)
|
|
34
47
|
dispatcher.registry.bluebooks.each_value do |bluebook|
|
|
35
48
|
chapter = chapter_module(dispatcher, bluebook)
|
data/lib/hecks/facade.rb
CHANGED
|
@@ -7,15 +7,15 @@ module Hecks
|
|
|
7
7
|
|
|
8
8
|
# The one careful way to put something at top level.
|
|
9
9
|
#
|
|
10
|
-
# Overwrites only what it itself installed (tracked in GENERATED), refuses —
|
|
10
|
+
# Overwrites only what it itself installed (tracked in `GENERATED`), refuses —
|
|
11
11
|
# with a warning, never a raise — to clobber a constant belonging to user
|
|
12
12
|
# code or the stdlib. Which is why a chapter named `Set` never becomes a
|
|
13
13
|
# constant, and why `Registry` keeps its own table of chapters rather than
|
|
14
|
-
# trusting Ruby's. Each re-install
|
|
15
|
-
# what GENERATED retains is one small facade module per name, not a graph
|
|
14
|
+
# trusting Ruby's. Each re-install replaces the previous boot's entry, so
|
|
15
|
+
# what `GENERATED` retains is one small facade module per name, not a graph
|
|
16
16
|
# per boot.
|
|
17
17
|
module Namespace
|
|
18
|
-
#
|
|
18
|
+
# Not frozen — a real registry, mutated below
|
|
19
19
|
# (`GENERATED[[container, name]] = value`). False positive for
|
|
20
20
|
# Style/MutableConstant.
|
|
21
21
|
# rubocop:disable-next Style/MutableConstant
|
|
@@ -23,6 +23,17 @@ module Hecks
|
|
|
23
23
|
|
|
24
24
|
module_function
|
|
25
25
|
|
|
26
|
+
# Sets `container::name` to `value`, replacing only a constant this module installed
|
|
27
|
+
# itself and leaving any other existing constant in place with a warning.
|
|
28
|
+
#
|
|
29
|
+
# @param container [Module] the namespace to define the constant in; every caller in
|
|
30
|
+
# this repository passes `Object`
|
|
31
|
+
# @param name [String, Symbol] the constant name, such as a chapter or aggregate name
|
|
32
|
+
# @param value [Module] the facade module to install under that name
|
|
33
|
+
# @return [Object] `value` when it was installed; otherwise the constant already
|
|
34
|
+
# defined under `name`, which this module does not own and did not replace
|
|
35
|
+
# @raise [NameError] if `name` is not a valid constant name (raised by
|
|
36
|
+
# `const_defined?`)
|
|
26
37
|
def install(container, name, value)
|
|
27
38
|
name = name.to_s
|
|
28
39
|
|
data/lib/hecks/forms/app.rb
CHANGED
|
@@ -14,17 +14,24 @@ module Hecks
|
|
|
14
14
|
module Forms
|
|
15
15
|
# The content-negotiated router: `GET /Banking/Account/Overdrawn.html`
|
|
16
16
|
# renders the query view built in this directory; the identical path
|
|
17
|
-
# with no extension (or `.json`) dispatches the
|
|
17
|
+
# with no extension (or `.json`) dispatches the same ask through
|
|
18
18
|
# `Runtime::Dispatcher#query` and answers with its raw result — one
|
|
19
19
|
# route, two representations, exactly the "change the file format on
|
|
20
20
|
# the route" mechanism this was built around
|
|
21
21
|
# (docs/command-form-and-query-form-bluebook.md).
|
|
22
22
|
#
|
|
23
23
|
# A plain Rack app (`#call(env)`) — no Sinatra, no Rails. `rack` itself
|
|
24
|
-
# is a
|
|
24
|
+
# is a lazy dependency the same way `pg`/`oauth2`/`aws-sdk-lambda` are
|
|
25
25
|
# for their own adapters (see the Gemfile's own comment) : a project
|
|
26
26
|
# that never boots this file never needs it installed.
|
|
27
27
|
class App
|
|
28
|
+
# Builds the Rack app for a configured app name, exposing exactly the chapters its
|
|
29
|
+
# `Forms.configure` block declared.
|
|
30
|
+
#
|
|
31
|
+
# @param registry [Runtime::Registry] the booted registry holding the exposed chapters
|
|
32
|
+
# @param app_name [String, Symbol] the name an earlier `Forms.configure` call registered
|
|
33
|
+
# @return [Forms::App] a Rack app routing only the configured chapters
|
|
34
|
+
# @raise [ArgumentError] if no app of that name has been configured
|
|
28
35
|
def self.for(registry:, app_name:)
|
|
29
36
|
config = Forms.config(app_name) ||
|
|
30
37
|
raise(ArgumentError, "no app #{app_name.inspect} configured — " \
|
|
@@ -32,12 +39,23 @@ module Hecks
|
|
|
32
39
|
new(registry: registry, exposed: config.exposes)
|
|
33
40
|
end
|
|
34
41
|
|
|
42
|
+
# @param registry [Runtime::Registry] the booted registry holding the exposed chapters
|
|
43
|
+
# @param exposed [Array<String>] names of the chapters (domains) this app routes; a
|
|
44
|
+
# request for any other domain is answered 404
|
|
45
|
+
# @param dispatcher [Runtime::Dispatcher, nil] the dispatcher commands and queries go
|
|
46
|
+
# through; nil builds a `Runtime::Dispatcher` over `registry`
|
|
35
47
|
def initialize(registry:, exposed:, dispatcher: nil)
|
|
36
48
|
@registry = registry
|
|
37
49
|
@exposed = exposed
|
|
38
50
|
@dispatcher = dispatcher || Runtime::Dispatcher.new(registry)
|
|
39
51
|
end
|
|
40
52
|
|
|
53
|
+
# Answers one Rack request, routing on the path and its trailing `.html`/`.json` format;
|
|
54
|
+
# an unknown or unexposed route is answered as a plain-text 404 rather than raised.
|
|
55
|
+
#
|
|
56
|
+
# @param env [Hash{String => Object}] the Rack environment for the request
|
|
57
|
+
# @return [Array(Integer, Hash{String => String}, Array<String>)] the Rack response
|
|
58
|
+
# triple of status, headers and body
|
|
41
59
|
def call(env)
|
|
42
60
|
request = Rack::Request.new(env)
|
|
43
61
|
route(request)
|
|
@@ -81,19 +99,18 @@ module Hecks
|
|
|
81
99
|
raise RouteNotFound, "#{domain.inspect} is not exposed by this app — declared chapters: #{@exposed.join(', ')}"
|
|
82
100
|
end
|
|
83
101
|
|
|
84
|
-
# H12 (docs/audits/2026-08-10-main-bug-audit.md) —
|
|
85
|
-
#
|
|
102
|
+
# H12 (docs/audits/2026-08-10-main-bug-audit.md) — only a literal
|
|
103
|
+
# trailing ".html"/".json" counts as a format; every other dot in the
|
|
104
|
+
# segment is just part of the identity. Splitting on the first "."
|
|
105
|
+
# instead truncates any identity value containing a dot (an email
|
|
86
106
|
# `identified_by { email.address }`, a decimal-ish reference — an
|
|
87
107
|
# aggregate's identity is free-form unless its value object declares
|
|
88
108
|
# a `pattern:`, see S3 in the same audit) at its own first dot, so
|
|
89
|
-
# `reference.value=c.1`
|
|
90
|
-
# its own index-table link.
|
|
91
|
-
# now counts as a format — every other dot in the segment is just
|
|
92
|
-
# part of the identity. An identity that itself happens to end in
|
|
109
|
+
# `reference.value=c.1` 404s everywhere: detail page, JSON view, and
|
|
110
|
+
# its own index-table link. An identity that itself happens to end in
|
|
93
111
|
# exactly ".html" or ".json" is still ambiguous with a real format
|
|
94
|
-
# suffix
|
|
95
|
-
# scheme has
|
|
96
|
-
# this bug.
|
|
112
|
+
# suffix — the same tension any extension-based content-negotiation
|
|
113
|
+
# scheme has, and not what H12 is about.
|
|
97
114
|
def split_format(segment)
|
|
98
115
|
segment = segment.to_s
|
|
99
116
|
return [Regexp.last_match(1), Regexp.last_match(2)] if segment =~ /\A(.*)\.(html|json)\z/
|
|
@@ -123,11 +140,11 @@ module Hecks
|
|
|
123
140
|
breadcrumbs: [[chapter.name, "/"], [aggregate.hecks_name, nil]])
|
|
124
141
|
else
|
|
125
142
|
instances = @registry.repository(chapter.name, aggregate).all
|
|
126
|
-
# id
|
|
143
|
+
# id last — see Instance#to_h's own comment: an aggregate free
|
|
127
144
|
# to declare its own attribute literally named `id` has that
|
|
128
145
|
# attribute's own wrapped value sitting in `i.state[:id]`
|
|
129
|
-
# already, which
|
|
130
|
-
#
|
|
146
|
+
# already, which silently clobbers the correct bare identity
|
|
147
|
+
# if `id:` is merged first.
|
|
131
148
|
json(200, instances.map { |i| i.state.merge(id: i.id) })
|
|
132
149
|
end
|
|
133
150
|
end
|
|
@@ -141,14 +158,13 @@ module Hecks
|
|
|
141
158
|
# L11 (docs/audits/2026-08-10-main-bug-audit.md) — a record's own
|
|
142
159
|
# id is free-form (S3) and can collide with one of its own
|
|
143
160
|
# aggregate's command/query names ("Close", "Overdrawn", ...).
|
|
144
|
-
# A GET for such an id must still be able to reach that
|
|
161
|
+
# A GET for such an id must still be able to reach that record's
|
|
145
162
|
# own detail page when a record with that literal id actually
|
|
146
|
-
# exists — checking the verb first
|
|
147
|
-
#
|
|
148
|
-
#
|
|
149
|
-
#
|
|
150
|
-
#
|
|
151
|
-
# as before.
|
|
163
|
+
# exists — checking the verb first would mean a record unlucky
|
|
164
|
+
# enough to be named after a real verb could never be viewed.
|
|
165
|
+
# POST never means "view a record" at all (`record_route` only
|
|
166
|
+
# ever answers GET), so command submission there is unambiguous
|
|
167
|
+
# and matches the verb first.
|
|
152
168
|
if request.get? && (instance = @registry.repository(domain, aggregate).find(verb_or_id))
|
|
153
169
|
return record_route(request, domain, aggregate, verb_or_id, format, instance: instance)
|
|
154
170
|
end
|
|
@@ -187,7 +203,7 @@ module Hecks
|
|
|
187
203
|
|
|
188
204
|
def submit_command(request, domain, aggregate, command, action)
|
|
189
205
|
raw, envelope = submitted_command(request, aggregate, command)
|
|
190
|
-
result = @dispatcher.
|
|
206
|
+
result = @dispatcher.dispatch_flat("#{domain}::#{aggregate.hecks_name}.#{command.hecks_name}", envelope)
|
|
191
207
|
# L12 — the id is free-form (S3), so it must be percent-encoded as
|
|
192
208
|
# a path segment here, not just interpolated raw.
|
|
193
209
|
redirect("/#{domain}/#{aggregate.hecks_name}/#{Escape.path(result.id)}.html")
|
|
@@ -201,8 +217,8 @@ module Hecks
|
|
|
201
217
|
return respond(405, "text/plain", "GET or POST only") unless request.post?
|
|
202
218
|
|
|
203
219
|
_, envelope = submitted_command(request, aggregate, command)
|
|
204
|
-
result = @dispatcher.
|
|
205
|
-
# id
|
|
220
|
+
result = @dispatcher.dispatch_flat("#{domain}::#{aggregate.hecks_name}.#{command.hecks_name}", envelope)
|
|
221
|
+
# id last — same reasoning as the other JSON-serializing call
|
|
206
222
|
# sites in this file (see aggregate_route's own comment).
|
|
207
223
|
json(201, result.state.merge(id: result.id))
|
|
208
224
|
rescue *Runtime::DOMAIN_REFUSALS, ArgumentError, TypeError, JSON::ParserError => e
|
|
@@ -239,7 +255,7 @@ module Hecks
|
|
|
239
255
|
results, error = run_query(domain, aggregate, query, fields, asked)
|
|
240
256
|
return json(422, { error: error.class.name.split("::").last, message: error.message }) if error
|
|
241
257
|
|
|
242
|
-
# id
|
|
258
|
+
# id last — same reasoning as the other JSON-serializing call
|
|
243
259
|
# sites in this file (see aggregate_route's own comment).
|
|
244
260
|
json(200, results.map { |i| i.state.merge(id: i.id) })
|
|
245
261
|
end
|
|
@@ -259,10 +275,10 @@ module Hecks
|
|
|
259
275
|
# JSON (the honest fallback for a multi-attribute list element this
|
|
260
276
|
# prototype's textarea doesn't build a second widget for). A caller
|
|
261
277
|
# who types a non-JSON line into that field raises `JSON::ParserError`
|
|
262
|
-
#
|
|
263
|
-
#
|
|
264
|
-
#
|
|
265
|
-
#
|
|
278
|
+
# before dispatch ever sees it. It is rescued here, as both command
|
|
279
|
+
# submission paths rescue it (`submit_command`, `command_json`), so
|
|
280
|
+
# a malformed list-of-VO query shows the same 422 every other
|
|
281
|
+
# bad-input path shows rather than a 500.
|
|
266
282
|
rescue *Runtime::DOMAIN_REFUSALS, ArgumentError, TypeError, JSON::ParserError => e
|
|
267
283
|
[nil, e]
|
|
268
284
|
end
|
|
@@ -272,7 +288,7 @@ module Hecks
|
|
|
272
288
|
|
|
273
289
|
instance ||= @registry.repository(domain, aggregate).find(id)
|
|
274
290
|
return not_found(aggregate, id, format) unless instance
|
|
275
|
-
# id
|
|
291
|
+
# id last — same reasoning as the other JSON-serializing call
|
|
276
292
|
# sites in this file (see aggregate_route's own comment).
|
|
277
293
|
return json(200, instance.state.merge(id: instance.id)) if format != "html"
|
|
278
294
|
|
|
@@ -13,22 +13,46 @@ module Hecks
|
|
|
13
13
|
# renders through here — nothing here is per-command code; see that
|
|
14
14
|
# doc for the 1:1 rule this deliberately holds to for now.
|
|
15
15
|
module CommandFormRenderer
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
# both nil/`{}` for a fresh form. `prefill` carries values a caller
|
|
21
|
-
# arrived WITH (typically `?to=...` off a record's own detail page) —
|
|
22
|
-
# kept separate from `values` because a prefill is not an error retry
|
|
23
|
-
# and should not be treated as one by a future reader of this code.
|
|
24
|
-
# The SAME field list a POST handler needs to cast raw params against
|
|
16
|
+
# Lists every field a command's form shows: the `to` identity picker first (unless the
|
|
17
|
+
# command creates its aggregate), then one field per command attribute.
|
|
18
|
+
#
|
|
19
|
+
# The same field list a POST handler needs to cast raw params against
|
|
25
20
|
# (params.rb's `Params.extract`) — one derivation, so a page never
|
|
26
21
|
# renders an input the submit handler doesn't also expect.
|
|
22
|
+
#
|
|
23
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate that declares the command
|
|
24
|
+
# @param command [Bluebook::Command] the command the form submits
|
|
25
|
+
# @return [Array<Forms::Field>] the form's top-level fields, in render order
|
|
26
|
+
# @raise [Bluebook::DSL::Malformed] if a `reference_to` attribute cannot say which
|
|
27
|
+
# aggregate declares it
|
|
27
28
|
def self.fields_for(aggregate, command)
|
|
28
29
|
addressing = command.creates? ? [] : [identity_field(aggregate)]
|
|
29
30
|
addressing + command.attributes.map { |a| FieldShape.resolve(a, aggregate: aggregate) }
|
|
30
31
|
end
|
|
31
32
|
|
|
33
|
+
# Renders the page body for one command: header, refusal banner, the form itself, and
|
|
34
|
+
# the inspect panel.
|
|
35
|
+
#
|
|
36
|
+
# `registry` serves only to populate a `:reference` field's
|
|
37
|
+
# `<select>` with real records (including the identity picker itself,
|
|
38
|
+
# for a non-creating command — see `identity_field` below). `values`/
|
|
39
|
+
# `error` carry a sticky re-render after a refused submission; leave
|
|
40
|
+
# both nil for a fresh form. `prefill` carries values a caller
|
|
41
|
+
# arrived with (typically `?to=...` off a record's own detail page) —
|
|
42
|
+
# kept separate from `values` because a prefill is not an error retry
|
|
43
|
+
# and should not be treated as one by a future reader of this code.
|
|
44
|
+
#
|
|
45
|
+
# @param registry [Runtime::Registry] the booted registry, read for reference options
|
|
46
|
+
# @param domain [String] name of the domain (chapter) the aggregate belongs to
|
|
47
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate that declares the command
|
|
48
|
+
# @param command [Bluebook::Command] the command the form submits
|
|
49
|
+
# @param action [String] URL path the form posts to
|
|
50
|
+
# @param values [Hash{String => Object}, nil] the raw submission to re-show after a
|
|
51
|
+
# refusal, keyed by dotted field path; nil falls back to `prefill`
|
|
52
|
+
# @param error [Exception, nil] the refusal to show in the banner; nil for a fresh form
|
|
53
|
+
# @param prefill [Hash{String => String}] values the caller arrived with, keyed by
|
|
54
|
+
# dotted field path; ignored when `values` is given
|
|
55
|
+
# @return [String] the HTML page body, without the surrounding page chrome
|
|
32
56
|
def self.render(registry:, domain:, aggregate:, command:, action:, values: nil, error: nil, prefill: {})
|
|
33
57
|
all_fields = fields_for(aggregate, command)
|
|
34
58
|
reference_options = ReferenceOptions.collect(registry, domain, all_fields)
|
|
@@ -48,12 +72,24 @@ module Hecks
|
|
|
48
72
|
HTML
|
|
49
73
|
end
|
|
50
74
|
|
|
75
|
+
# Builds the `to` field a non-creating command uses to name the record it acts on,
|
|
76
|
+
# labelled with the aggregate's identity paths.
|
|
77
|
+
#
|
|
78
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate whose records the picker offers
|
|
79
|
+
# @return [Forms::Field] a `:reference` field at path `"to"` targeting `aggregate`
|
|
51
80
|
def self.identity_field(aggregate)
|
|
52
81
|
Field.new(path: "to", label: "#{aggregate.hecks_name} (#{aggregate.identity_paths.join(', ')})",
|
|
53
82
|
kind: :reference, html_type: "text", target_aggregate: aggregate,
|
|
54
83
|
help: "The record this command acts on.")
|
|
55
84
|
end
|
|
56
85
|
|
|
86
|
+
# Renders the form's heading: the command's qualified name, its role and creates
|
|
87
|
+
# badges, its goal, and its preconditions callout.
|
|
88
|
+
#
|
|
89
|
+
# @param domain [String] name of the domain (chapter) the aggregate belongs to
|
|
90
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate that declares the command
|
|
91
|
+
# @param command [Bluebook::Command] the command being described
|
|
92
|
+
# @return [String] HTML for the heading block
|
|
57
93
|
def self.header(domain, aggregate, command)
|
|
58
94
|
<<~HTML
|
|
59
95
|
<h1>#{Escape.html("#{domain}::#{aggregate.hecks_name}.#{command.hecks_name}")}</h1>
|
|
@@ -64,6 +100,11 @@ module Hecks
|
|
|
64
100
|
HTML
|
|
65
101
|
end
|
|
66
102
|
|
|
103
|
+
# Renders the command's `given` preconditions as a callout, so a caller sees what
|
|
104
|
+
# would refuse the submission before making it.
|
|
105
|
+
#
|
|
106
|
+
# @param command [Bluebook::Command] the command whose givens are listed
|
|
107
|
+
# @return [String] HTML for the callout, or `""` when the command declares no givens
|
|
67
108
|
def self.givens_callout(command)
|
|
68
109
|
return "" if command.givens.empty?
|
|
69
110
|
|
|
@@ -71,6 +112,11 @@ module Hecks
|
|
|
71
112
|
%(<div class="callout"><strong>Preconditions</strong> — refused if any fail:<ul>#{items.join}</ul></div>)
|
|
72
113
|
end
|
|
73
114
|
|
|
115
|
+
# Renders a refused submission as an alert banner carrying the refusal's class name
|
|
116
|
+
# (without its namespace) and message.
|
|
117
|
+
#
|
|
118
|
+
# @param error [Exception, nil] the refusal raised by the submission
|
|
119
|
+
# @return [String] HTML for the banner, or `""` when `error` is nil
|
|
74
120
|
def self.error_banner(error)
|
|
75
121
|
return "" unless error
|
|
76
122
|
|
|
@@ -81,14 +127,29 @@ module Hecks
|
|
|
81
127
|
HTML
|
|
82
128
|
end
|
|
83
129
|
|
|
130
|
+
# Maps a refusal to per-field messages; always empty, so every refusal is shown in
|
|
131
|
+
# the banner rather than beside a field.
|
|
132
|
+
#
|
|
84
133
|
# No structured field attribution exists on a domain refusal today
|
|
85
134
|
# (it is a typed exception with a rendered message — see
|
|
86
135
|
# docs/command-form-and-query-form-bluebook.md's note on
|
|
87
136
|
# `RefusalWording`), so this returns empty rather than guessing which
|
|
88
137
|
# field a message meant; the banner above carries the real text
|
|
89
138
|
# instead of a misattributed hint.
|
|
139
|
+
#
|
|
140
|
+
# @param _error [Exception, nil] the refusal raised by the submission; ignored
|
|
141
|
+
# @return [Hash] always empty; keys would be dotted field paths
|
|
90
142
|
def self.field_errors(_error) = {}
|
|
91
143
|
|
|
144
|
+
# Renders the collapsed "Inspect" panel: the events the command emits, an equivalent
|
|
145
|
+
# `curl` request, its field paths, and its declaration as JSON.
|
|
146
|
+
#
|
|
147
|
+
# @param domain [String] name of the domain (chapter) the aggregate belongs to
|
|
148
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate that declares the command
|
|
149
|
+
# @param command [Bluebook::Command] the command being described
|
|
150
|
+
# @param action [String] URL path the form posts to, shown in the `curl` line
|
|
151
|
+
# @param fields [Array<Forms::Field>] the form's fields, as `fields_for` returns them
|
|
152
|
+
# @return [String] HTML for the `<details>` panel
|
|
92
153
|
def self.inspect_panel(domain, aggregate, command, action, fields)
|
|
93
154
|
verb = "#{domain}::#{aggregate.hecks_name}.#{command.hecks_name}"
|
|
94
155
|
paths = Params.paths(fields)
|