hecks 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/hecks/adapters/driven/claude_code.rb +72 -7
- data/lib/hecks/adapters/driven/d1.rb +187 -23
- data/lib/hecks/adapters/driven/folder.rb +83 -10
- data/lib/hecks/adapters/driven/google_authentication.rb +33 -12
- data/lib/hecks/adapters/driven/governance_authorization.rb +87 -18
- data/lib/hecks/adapters/driven/heki/journal.rb +13 -4
- data/lib/hecks/adapters/driven/heki/saga_store.rb +56 -10
- data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
- data/lib/hecks/adapters/driven/heki.rb +106 -9
- data/lib/hecks/adapters/driven/identity_registry.rb +12 -2
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +25 -3
- data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
- data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
- data/lib/hecks/adapters/driven/lambda/client.rb +67 -14
- data/lib/hecks/adapters/driven/lambda.rb +82 -35
- data/lib/hecks/adapters/driven/local_storage.rb +83 -10
- data/lib/hecks/adapters/driven/memory.rb +205 -9
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +21 -1
- data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
- data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
- data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
- data/lib/hecks/adapters/driven/postgres.rb +175 -28
- data/lib/hecks/adapters/driven/prism.rb +50 -11
- data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
- data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
- data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
- data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
- data/lib/hecks/adapters/driven/sqlite.rb +181 -21
- data/lib/hecks/adapters/driven/system_clock.rb +3 -0
- data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
- data/lib/hecks/adapters/driven.rb +6 -4
- data/lib/hecks/adapters/driving/github_webhook.rb +31 -18
- data/lib/hecks/behaviors/dsl.rb +60 -2
- data/lib/hecks/behaviors/expectations.rb +190 -29
- data/lib/hecks/behaviors/ir.rb +12 -1
- data/lib/hecks/behaviors/rspec.rb +9 -1
- data/lib/hecks/behaviors/runner.rb +21 -2
- data/lib/hecks/behaviors.rb +9 -1
- data/lib/hecks/bluebook/aggregate.rb +43 -13
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +17 -10
- data/lib/hecks/bluebook/assembly/build.rb +26 -5
- data/lib/hecks/bluebook/assembly/contract.rb +98 -23
- data/lib/hecks/bluebook/assembly/contracts.rb +59 -52
- data/lib/hecks/bluebook/assembly/marks.rb +159 -30
- data/lib/hecks/bluebook/assembly/specializer.rb +38 -21
- data/lib/hecks/bluebook/assembly.rb +32 -14
- data/lib/hecks/bluebook/attribute.rb +26 -12
- data/lib/hecks/bluebook/behaviour/aggregate.rb +43 -11
- data/lib/hecks/bluebook/behaviour/attribute.rb +18 -5
- data/lib/hecks/bluebook/behaviour/chapter.rb +76 -5
- data/lib/hecks/bluebook/behaviour/command.rb +55 -25
- data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -7
- data/lib/hecks/bluebook/behaviour/entity.rb +20 -8
- data/lib/hecks/bluebook/behaviour/hexagon.rb +30 -4
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +27 -6
- data/lib/hecks/bluebook/behaviour/policy.rb +42 -17
- data/lib/hecks/bluebook/behaviour/process_manager.rb +39 -8
- data/lib/hecks/bluebook/behaviour/query.rb +6 -1
- data/lib/hecks/bluebook/behaviour/read_model.rb +29 -8
- data/lib/hecks/bluebook/behaviour/traits.rb +48 -12
- data/lib/hecks/bluebook/behaviour/value_object.rb +21 -9
- data/lib/hecks/bluebook/capabilities.rb +27 -0
- data/lib/hecks/bluebook/chapter.rb +51 -9
- data/lib/hecks/bluebook/command.rb +62 -17
- data/lib/hecks/bluebook/domain_port.rb +34 -9
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +282 -123
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +112 -75
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +81 -2
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +486 -117
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +179 -47
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +284 -122
- data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
- data/lib/hecks/bluebook/dsl/entity_builder.rb +191 -61
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +130 -30
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +38 -21
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
- data/lib/hecks/bluebook/dsl/policy_builder.rb +86 -36
- data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +111 -47
- data/lib/hecks/bluebook/dsl/query_builder.rb +37 -8
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +127 -52
- data/lib/hecks/bluebook/dsl/rule_reference.rb +97 -43
- data/lib/hecks/bluebook/dsl/translation_builder.rb +150 -44
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +68 -20
- data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
- data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
- data/lib/hecks/bluebook/entity.rb +40 -11
- data/lib/hecks/bluebook/expression/ast_json.rb +128 -36
- data/lib/hecks/bluebook/expression/ast_reader.rb +32 -3
- data/lib/hecks/bluebook/expression/canonical_form.rb +55 -16
- data/lib/hecks/bluebook/expression/evaluator.rb +221 -43
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +54 -18
- data/lib/hecks/bluebook/expression/resolver.rb +369 -128
- data/lib/hecks/bluebook/hexagon.rb +35 -1
- data/lib/hecks/bluebook/lifecycle.rb +12 -1
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +2 -1
- data/lib/hecks/bluebook/meta_validator/judge.rb +126 -108
- data/lib/hecks/bluebook/meta_validator/plan.rb +81 -46
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +3 -2
- data/lib/hecks/bluebook/meta_validator/readings.rb +200 -50
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +68 -41
- data/lib/hecks/bluebook/meta_validator/shapes.rb +166 -21
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +286 -46
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +11 -10
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -5
- data/lib/hecks/bluebook/meta_validator.rb +235 -139
- data/lib/hecks/bluebook/model_check.rb +434 -104
- data/lib/hecks/bluebook/pattern_subset.rb +32 -10
- data/lib/hecks/bluebook/policy.rb +15 -13
- data/lib/hecks/bluebook/process_manager.rb +27 -14
- data/lib/hecks/bluebook/project_discovery.rb +5 -0
- data/lib/hecks/bluebook/project_loader.rb +40 -0
- data/lib/hecks/bluebook/project_register.rb +50 -6
- data/lib/hecks/bluebook/query.rb +31 -4
- data/lib/hecks/bluebook/read_model.rb +35 -15
- data/lib/hecks/bluebook/reference.rb +26 -13
- data/lib/hecks/bluebook/smoke_test.rb +46 -23
- data/lib/hecks/bluebook/synthesizer.rb +46 -12
- data/lib/hecks/bluebook/translation.rb +34 -5
- data/lib/hecks/bluebook/value_object.rb +29 -11
- data/lib/hecks/bluebook.rb +5 -6
- data/lib/hecks/codemod.rb +138 -50
- data/lib/hecks/construct.rb +21 -7
- data/lib/hecks/corpus.rb +438 -0
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
- data/lib/hecks/doc/reference.rb +200 -31
- data/lib/hecks/embryonaut_bluebook.rb +38 -15
- data/lib/hecks/facade/cli_door.rb +69 -10
- data/lib/hecks/facade/cli_runner.rb +105 -24
- data/lib/hecks/facade/command_request.rb +23 -0
- data/lib/hecks/facade/handle.rb +155 -35
- data/lib/hecks/facade/json_door.rb +106 -25
- data/lib/hecks/facade/surface/aggregate_door.rb +50 -27
- data/lib/hecks/facade/surface/chapter.rb +26 -17
- data/lib/hecks/facade/surface.rb +16 -3
- data/lib/hecks/facade.rb +15 -4
- data/lib/hecks/forms/app.rb +46 -30
- data/lib/hecks/forms/command_form_renderer.rb +70 -9
- data/lib/hecks/forms/field_renderer.rb +142 -6
- data/lib/hecks/forms/field_shape.rb +183 -19
- data/lib/hecks/forms/html.rb +51 -7
- data/lib/hecks/forms/index_renderer.rb +14 -2
- data/lib/hecks/forms/page.rb +14 -0
- data/lib/hecks/forms/params.rb +120 -23
- data/lib/hecks/forms/port_argument.rb +14 -2
- data/lib/hecks/forms/query_form_renderer.rb +65 -2
- data/lib/hecks/forms/record_renderer.rb +60 -2
- data/lib/hecks/forms/record_table.rb +28 -1
- data/lib/hecks/forms/reference_options.rb +24 -0
- data/lib/hecks/forms/value_object_shape.rb +13 -3
- data/lib/hecks/forms.rb +24 -4
- data/lib/hecks/fqn.rb +59 -1
- data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
- data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
- data/lib/hecks/framework/oidc.json +15 -0
- data/lib/hecks/framework.rb +79 -25
- data/lib/hecks/freezer.rb +27 -11
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +241 -106
- data/lib/hecks/fuzzing/combination_miner.rb +178 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +241 -45
- data/lib/hecks/fuzzing/coverage_campaign.rb +161 -0
- data/lib/hecks/fuzzing/differential.rb +192 -0
- data/lib/hecks/fuzzing/domain_generator.rb +871 -0
- data/lib/hecks/fuzzing/era_boundary.rb +68 -19
- data/lib/hecks/fuzzing/form_census.rb +121 -20
- data/lib/hecks/fuzzing/generated_domain_check.rb +171 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +45 -6
- data/lib/hecks/fuzzing/isolated_boot.rb +137 -80
- data/lib/hecks/fuzzing/nondeterministic.rb +79 -0
- data/lib/hecks/fuzzing/persistence_parity.rb +111 -21
- data/lib/hecks/fuzzing/properties/corrections.rb +34 -9
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +300 -51
- data/lib/hecks/fuzzing/properties/guards.rb +86 -42
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +80 -32
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +40 -35
- data/lib/hecks/fuzzing/properties/outbox.rb +70 -32
- data/lib/hecks/fuzzing/properties/querying.rb +82 -28
- data/lib/hecks/fuzzing/properties.rb +84 -51
- data/lib/hecks/fuzzing/qa_settings.rb +164 -0
- data/lib/hecks/fuzzing/replay.rb +241 -133
- data/lib/hecks/fuzzing/rotation_priority.rb +48 -28
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +139 -0
- data/lib/hecks/fuzzing/self_consistency.rb +315 -137
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +46 -40
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +59 -32
- data/lib/hecks/fuzzing/sequence_generator.rb +158 -23
- data/lib/hecks/fuzzing/shrinker.rb +309 -0
- data/lib/hecks/fuzzing/structural_skips.rb +37 -130
- data/lib/hecks/fuzzing/sweep_depth.rb +12 -4
- data/lib/hecks/fuzzing/target_capabilities.rb +107 -29
- data/lib/hecks/fuzzing/value_generator.rb +110 -22
- data/lib/hecks/fuzzing.rb +5 -0
- data/lib/hecks/grammar/evolve.rb +188 -12
- data/lib/hecks/grammar.rb +53 -7
- data/lib/hecks/ir.rb +51 -20
- data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
- data/lib/hecks/language/bluebook/policy.bluebook +11 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
- data/lib/hecks/language/oidc.json +5 -0
- data/lib/hecks/literal.rb +41 -9
- data/lib/hecks/naming.rb +112 -31
- data/lib/hecks/ports/access_control.rb +53 -2
- data/lib/hecks/ports/agent/answers.rb +83 -6
- data/lib/hecks/ports/agent.rb +119 -35
- data/lib/hecks/ports/authentication.rb +44 -4
- data/lib/hecks/ports/authorization.rb +50 -11
- data/lib/hecks/ports/clock.rb +42 -23
- data/lib/hecks/ports/extraction.rb +16 -0
- data/lib/hecks/ports/identity_assignment.rb +23 -2
- data/lib/hecks/ports/identity_generation.rb +17 -3
- data/lib/hecks/ports/identity_resolution.rb +17 -1
- data/lib/hecks/ports/key_vault.port +6 -0
- data/lib/hecks/ports/key_vault.rb +58 -0
- data/lib/hecks/ports/loading.rb +4 -0
- data/lib/hecks/ports/persistence/append_only.rb +172 -8
- data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
- data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
- data/lib/hecks/ports/persistence/execution.rb +4 -0
- data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
- data/lib/hecks/ports/persistence/plugin.rb +42 -4
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
- data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +34 -5
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +95 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
- data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +48 -8
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +58 -19
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +96 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +9 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
- data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
- data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
- data/lib/hecks/ports/persistence/state_codec.rb +319 -0
- data/lib/hecks/ports/persistence.rb +36 -1
- data/lib/hecks/ports/projection.rb +61 -7
- data/lib/hecks/ports/query/in_memory.rb +41 -8
- data/lib/hecks/ports/query/ordering.rb +21 -6
- data/lib/hecks/ports/query.rb +35 -0
- data/lib/hecks/ports.rb +1 -0
- data/lib/hecks/projections/bootstrap_table.rb +147 -0
- data/lib/hecks/projections/diagrams.rb +315 -79
- data/lib/hecks/projections/glossary/html.rb +91 -3
- data/lib/hecks/projections/glossary/markdown.rb +50 -3
- data/lib/hecks/projections/glossary/mermaid.rb +49 -1
- data/lib/hecks/projections/glossary/sections.rb +1 -1
- data/lib/hecks/projections/glossary/sentences.rb +110 -5
- data/lib/hecks/projections/glossary.rb +173 -31
- data/lib/hecks/projections/ir.rb +1 -1
- data/lib/hecks/projections/model/deviations.rb +62 -17
- data/lib/hecks/projections/model.rb +75 -21
- data/lib/hecks/projections/oidc.rb +45 -8
- data/lib/hecks/projections/parser_table.rb +53 -12
- data/lib/hecks/projections/reference.rb +15 -3
- data/lib/hecks/projections/rust_vocabulary.rb +646 -0
- data/lib/hecks/projections/shape.rb +10 -2
- data/lib/hecks/projections/statements.rb +69 -22
- data/lib/hecks/projections/vocabulary.rb +26 -9
- data/lib/hecks/projections.rb +5 -3
- data/lib/hecks/projector/cli_projector.rb +245 -37
- data/lib/hecks/projector/docs_projector.rb +154 -28
- data/lib/hecks/projector/exporter.rb +104 -29
- data/lib/hecks/projector/ir_projector.rb +7 -1
- data/lib/hecks/projector/narrate_projector.rb +144 -23
- data/lib/hecks/projector/target.rb +42 -18
- data/lib/hecks/projector.rb +86 -18
- data/lib/hecks/query_ir.rb +94 -47
- data/lib/hecks/query_specification/common/comparators.rb +19 -3
- data/lib/hecks/query_specification/common/comparison.rb +109 -27
- data/lib/hecks/query_specification/common/dsl.rb +65 -9
- data/lib/hecks/query_specification/common/null_policy.rb +61 -15
- data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
- data/lib/hecks/query_specification/common/options.rb +25 -0
- data/lib/hecks/query_specification/field_path.rb +69 -15
- data/lib/hecks/query_specification/hop_path.rb +57 -20
- data/lib/hecks/query_specification/read_model/specification.rb +4 -0
- data/lib/hecks/rendering.rb +9 -3
- data/lib/hecks/router/namespace_installer.rb +16 -3
- data/lib/hecks/router.rb +56 -1
- data/lib/hecks/runtime/aggregate_lock.rb +20 -11
- data/lib/hecks/runtime/boot_gates.rb +21 -3
- data/lib/hecks/runtime/caller.rb +40 -8
- data/lib/hecks/runtime/capability_graph.rb +13 -2
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +40 -41
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +45 -67
- data/lib/hecks/runtime/command_interpreter.rb +195 -113
- data/lib/hecks/runtime/command_rules/admissibility.rb +231 -80
- data/lib/hecks/runtime/command_rules/arithmetic.rb +154 -96
- data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
- data/lib/hecks/runtime/command_rules/emission.rb +18 -1
- data/lib/hecks/runtime/command_rules/references.rb +145 -55
- data/lib/hecks/runtime/command_rules.rb +3 -0
- data/lib/hecks/runtime/dependency_planning.rb +56 -11
- data/lib/hecks/runtime/dispatcher.rb +263 -92
- data/lib/hecks/runtime/entity_element.rb +304 -67
- data/lib/hecks/runtime/entity_interpreter.rb +149 -88
- data/lib/hecks/runtime/errors.rb +37 -23
- data/lib/hecks/runtime/event.rb +15 -10
- data/lib/hecks/runtime/identity.rb +91 -23
- data/lib/hecks/runtime/instance.rb +103 -18
- data/lib/hecks/runtime/interpreting.rb +24 -16
- data/lib/hecks/runtime/invocation.rb +358 -0
- data/lib/hecks/runtime/loader.rb +105 -19
- data/lib/hecks/runtime/outbox.rb +164 -26
- data/lib/hecks/runtime/policy_interpreter.rb +73 -60
- data/lib/hecks/runtime/port_operation_interpreter.rb +42 -19
- data/lib/hecks/runtime/query_interpreter.rb +93 -74
- data/lib/hecks/runtime/reaction_invocation.rb +73 -28
- data/lib/hecks/runtime/read_model_interpreter.rb +60 -44
- data/lib/hecks/runtime/rebuild_sweep.rb +32 -4
- data/lib/hecks/runtime/reference_hop.rb +48 -6
- data/lib/hecks/runtime/refusal_wording.rb +142 -115
- data/lib/hecks/runtime/registry/saga_persistence.rb +32 -21
- data/lib/hecks/runtime/registry/verification.rb +153 -28
- data/lib/hecks/runtime/registry.rb +202 -32
- data/lib/hecks/runtime/remote_dispatcher.rb +125 -24
- data/lib/hecks/runtime/routing.rb +36 -154
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +25 -27
- data/lib/hecks/runtime/saga_interpreter.rb +90 -76
- data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
- data/lib/hecks/runtime/tenant_check.rb +33 -13
- data/lib/hecks/runtime/tenant_scope.rb +23 -5
- data/lib/hecks/runtime/value/admission.rb +75 -30
- data/lib/hecks/runtime/value/coercion.rb +555 -142
- data/lib/hecks/runtime/value/entity_list_coercion.rb +132 -60
- data/lib/hecks/runtime/value.rb +71 -21
- data/lib/hecks/runtime.rb +39 -7
- data/lib/hecks/storehouse.rb +368 -72
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
- data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
- data/lib/hecks/version.rb +3 -3
- data/lib/hecks/vocabulary.rb +205 -4
- data/lib/hecks.rb +91 -11
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +19 -11
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +24 -12
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +38 -14
- metadata +28 -2
|
@@ -11,6 +11,12 @@ module Hecks
|
|
|
11
11
|
# do (facade/surface/aggregate_door.rb) — so they exist for every
|
|
12
12
|
# aggregate whether or not its bluebook declared a query at all.
|
|
13
13
|
module RecordRenderer
|
|
14
|
+
# Renders the index page listing every record of one aggregate.
|
|
15
|
+
#
|
|
16
|
+
# @param registry [Runtime::Registry] the booted registry holding the domain
|
|
17
|
+
# @param domain [String, Symbol] the domain name
|
|
18
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate whose records are listed
|
|
19
|
+
# @return [String] the index page's rendered HTML
|
|
14
20
|
def self.index(registry:, domain:, aggregate:)
|
|
15
21
|
instances = registry.repository(domain, aggregate).all
|
|
16
22
|
<<~HTML
|
|
@@ -23,6 +29,12 @@ module Hecks
|
|
|
23
29
|
HTML
|
|
24
30
|
end
|
|
25
31
|
|
|
32
|
+
# Renders the "+ Command" buttons for every creating command.
|
|
33
|
+
#
|
|
34
|
+
# @param domain [String, Symbol] the domain name
|
|
35
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate whose creating commands
|
|
36
|
+
# are linked
|
|
37
|
+
# @return [String] HTML action buttons, or `""` if the aggregate declares none
|
|
26
38
|
def self.creating_links(domain, aggregate)
|
|
27
39
|
creators = aggregate.commands.select(&:creates?)
|
|
28
40
|
return "" if creators.empty?
|
|
@@ -34,6 +46,15 @@ module Hecks
|
|
|
34
46
|
%(<div class="actions">#{links.join}</div>)
|
|
35
47
|
end
|
|
36
48
|
|
|
49
|
+
# Renders one record's state page: its current fields, plus the commands and
|
|
50
|
+
# queries it can dispatch next.
|
|
51
|
+
#
|
|
52
|
+
# @param registry [Runtime::Registry] the booted registry holding the domain
|
|
53
|
+
# @param domain [String, Symbol] the domain name
|
|
54
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate the record belongs to
|
|
55
|
+
# @param id [String, Object] the record's identity
|
|
56
|
+
# @return [String, nil] the record page's rendered HTML, or nil if no record
|
|
57
|
+
# has that id
|
|
37
58
|
def self.show(registry:, domain:, aggregate:, id:)
|
|
38
59
|
instance = registry.repository(domain, aggregate).find(id)
|
|
39
60
|
return nil unless instance
|
|
@@ -50,6 +71,10 @@ module Hecks
|
|
|
50
71
|
HTML
|
|
51
72
|
end
|
|
52
73
|
|
|
74
|
+
# Renders a record's state as an HTML field table.
|
|
75
|
+
#
|
|
76
|
+
# @param state [Hash{Symbol => Object}] the record's stored attribute values
|
|
77
|
+
# @return [String] an HTML table of the state's fields, values escaped
|
|
53
78
|
def self.state_table(state)
|
|
54
79
|
rows = state.map do |key, value|
|
|
55
80
|
"<tr><th>#{Escape.html(Humanize.label(key.to_s))}</th><td>#{Escape.html(render_value(value))}</td></tr>"
|
|
@@ -60,6 +85,10 @@ module Hecks
|
|
|
60
85
|
# `Value.materialize` first — a stored field is a `Runtime::Value`
|
|
61
86
|
# wherever its attribute is a value object, not a plain Hash (see
|
|
62
87
|
# record_table.rb's own note on the same read).
|
|
88
|
+
#
|
|
89
|
+
# @param value [Object] a stored field's value, possibly a `Runtime::Value`
|
|
90
|
+
# @return [String] the value rendered as text: a Hash as `"key: value, ..."`
|
|
91
|
+
# pairs, an Array `;`-joined, anything else its own `to_s`
|
|
63
92
|
def self.render_value(value)
|
|
64
93
|
case (value = Runtime::Value.materialize(value))
|
|
65
94
|
when Hash then value.map { |k, v| "#{k}: #{render_value(v)}" }.join(", ")
|
|
@@ -68,17 +97,25 @@ module Hecks
|
|
|
68
97
|
end
|
|
69
98
|
end
|
|
70
99
|
|
|
71
|
-
# Every non-creating command,
|
|
100
|
+
# Every non-creating command, except a lifecycle transition that does
|
|
72
101
|
# not apply from the record's current state — the same rule
|
|
73
102
|
# `Rules#admissible_transition` enforces at dispatch, read here so a
|
|
74
103
|
# link that would only refuse is never offered in the first place.
|
|
104
|
+
#
|
|
105
|
+
# @param domain [String, Symbol] the domain name
|
|
106
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate the record belongs to
|
|
107
|
+
# @param id [String, Object] the record's identity
|
|
108
|
+
# @param current_state [String, nil] the record's current lifecycle state, or
|
|
109
|
+
# nil if the aggregate has no lifecycle field or it is unset
|
|
110
|
+
# @return [String] an HTML list of admissible command links, or a message
|
|
111
|
+
# when no command currently applies
|
|
75
112
|
def self.command_links(domain, aggregate, id, current_state)
|
|
76
113
|
commands = aggregate.commands.reject(&:creates?).select { |cmd| applies?(aggregate, cmd, current_state) }
|
|
77
114
|
return "<p><em>No commands act on an existing #{Escape.html(aggregate.hecks_name)}.</em></p>" if commands.empty?
|
|
78
115
|
|
|
79
116
|
items = commands.map do |cmd|
|
|
80
117
|
# L12 — the id is free-form (S3): percent-encoded as the query
|
|
81
|
-
#
|
|
118
|
+
# value (a raw `&` here would smuggle a second bogus query
|
|
82
119
|
# parameter), then the assembled href is attribute-escaped as
|
|
83
120
|
# usual.
|
|
84
121
|
href = "/#{domain}/#{aggregate.hecks_name}/#{cmd.hecks_name}.html?to=#{Escape.url(id)}"
|
|
@@ -88,6 +125,14 @@ module Hecks
|
|
|
88
125
|
%(<ul class="verb-list">#{items.join}</ul>)
|
|
89
126
|
end
|
|
90
127
|
|
|
128
|
+
# Whether the aggregate's lifecycle admits dispatching `command` from
|
|
129
|
+
# `current_state`.
|
|
130
|
+
#
|
|
131
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate the command belongs to
|
|
132
|
+
# @param command [Bluebook::Command] the command to check
|
|
133
|
+
# @param current_state [String, nil] the record's current lifecycle state
|
|
134
|
+
# @return [Boolean] true if the aggregate declares no lifecycle transitions for
|
|
135
|
+
# this command, or one of them admits `current_state`
|
|
91
136
|
def self.applies?(aggregate, command, current_state)
|
|
92
137
|
transitions = aggregate.lifecycle&.transitions_for(command.hecks_name) || []
|
|
93
138
|
return true if transitions.empty?
|
|
@@ -95,6 +140,12 @@ module Hecks
|
|
|
95
140
|
transitions.any? { |t| t.from.nil? || Array(t.from).map(&:to_s).include?(current_state.to_s) }
|
|
96
141
|
end
|
|
97
142
|
|
|
143
|
+
# Renders the "Queries" link list for an aggregate's own queries.
|
|
144
|
+
#
|
|
145
|
+
# @param domain [String, Symbol] the domain name
|
|
146
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate whose queries are linked
|
|
147
|
+
# @return [String] an HTML section listing every query, or `""` if the
|
|
148
|
+
# aggregate declares none
|
|
98
149
|
def self.query_links(domain, aggregate)
|
|
99
150
|
return "" if aggregate.queries.empty?
|
|
100
151
|
|
|
@@ -105,6 +156,13 @@ module Hecks
|
|
|
105
156
|
%(<h2>Queries</h2><ul class="verb-list">#{items.join}</ul>)
|
|
106
157
|
end
|
|
107
158
|
|
|
159
|
+
# Renders the full command-and-query index for an aggregate, independent of
|
|
160
|
+
# any one record's state.
|
|
161
|
+
#
|
|
162
|
+
# @param domain [String, Symbol] the domain name
|
|
163
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate whose verbs are listed
|
|
164
|
+
# @return [String] an HTML section listing every non-creating command and every
|
|
165
|
+
# query, or `""` if the aggregate declares neither
|
|
108
166
|
def self.verb_list(domain, aggregate)
|
|
109
167
|
commands = aggregate.commands.reject(&:creates?)
|
|
110
168
|
return "" if commands.empty? && aggregate.queries.empty?
|
|
@@ -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
|
|
@@ -19,12 +19,26 @@ module Hecks
|
|
|
19
19
|
# query's own results (query_form_renderer.rb) — the same records
|
|
20
20
|
# read the same columns either way.
|
|
21
21
|
module RecordTable
|
|
22
|
+
# Picks which fields to show as columns: identity, lifecycle state, then scalar
|
|
23
|
+
# attributes, capped at seven.
|
|
24
|
+
#
|
|
25
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate whose records will be shown
|
|
26
|
+
# @return [Array<Symbol>] up to 7 unique field names, in that order; excludes
|
|
27
|
+
# reference and list attributes
|
|
22
28
|
def self.columns(aggregate)
|
|
23
29
|
lifecycle = aggregate.lifecycle&.field
|
|
24
30
|
scalars = aggregate.attributes.reject { |a| a.reference? || a.list? }.map(&:name)
|
|
25
31
|
[*aggregate.identity_heads, lifecycle, *scalars].compact.uniq.first(7)
|
|
26
32
|
end
|
|
27
33
|
|
|
34
|
+
# Renders a list of records as an HTML table, one row per record.
|
|
35
|
+
#
|
|
36
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate the records belong to
|
|
37
|
+
# @param instances [Array<Runtime::Instance, Forms::Record>] the records to render;
|
|
38
|
+
# anything answering `#id`/`#state` works
|
|
39
|
+
# @param domain [String] the owning chapter's name, for building each row's link
|
|
40
|
+
# @return [String] the table markup; `"<p><em>No records.</em></p>"` when
|
|
41
|
+
# `instances` is empty
|
|
28
42
|
def self.render(aggregate, instances, domain:)
|
|
29
43
|
cols = columns(aggregate)
|
|
30
44
|
head = (["id"] + cols).map { |name| "<th>#{Escape.html(Humanize.label(name.to_s))}</th>" }.join
|
|
@@ -39,6 +53,13 @@ module Hecks
|
|
|
39
53
|
HTML
|
|
40
54
|
end
|
|
41
55
|
|
|
56
|
+
# Renders one record's own table row, linked to its show page.
|
|
57
|
+
#
|
|
58
|
+
# @param instance [Runtime::Instance, Forms::Record] the record to render
|
|
59
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate the record belongs to
|
|
60
|
+
# @param cols [Array<Symbol>] the column field names, as `columns` returns them
|
|
61
|
+
# @param domain [String] the owning chapter's name, for building the row's link
|
|
62
|
+
# @return [String] the `<tr>` markup
|
|
42
63
|
def self.row(instance, aggregate, cols, domain)
|
|
43
64
|
cells = cols.map { |name| "<td>#{Escape.html(cell(instance, name))}</td>" }.join
|
|
44
65
|
# L12 — the id is free-form (S3): percent-encoded as the path
|
|
@@ -50,6 +71,12 @@ module Hecks
|
|
|
50
71
|
"<tr><td><a href=\"#{Escape.attr(href)}\">#{Escape.html(instance.id)}</a></td>#{cells}</tr>"
|
|
51
72
|
end
|
|
52
73
|
|
|
74
|
+
# Reads one field's value for display, unwrapped to a single cell value.
|
|
75
|
+
#
|
|
76
|
+
# @param instance [Runtime::Instance, Forms::Record] the record to read
|
|
77
|
+
# @param name [Symbol] the field name to read
|
|
78
|
+
# @return [Object] the field's own value; for a Hash-shaped value object, its
|
|
79
|
+
# first member's value; `""` for `nil`
|
|
53
80
|
def self.cell(instance, name)
|
|
54
81
|
# `state` holds `Runtime::Value` wherever an attribute is a value
|
|
55
82
|
# object, not a plain Hash — `.materialize` is the runtime's own
|
|
@@ -7,12 +7,27 @@ module Hecks
|
|
|
7
7
|
# resolve against the same repository the same way, and neither word
|
|
8
8
|
# owns this more than the other.
|
|
9
9
|
module ReferenceOptions
|
|
10
|
+
# Collects every `:reference` field's own dropdown options out of a resolved field
|
|
11
|
+
# tree.
|
|
12
|
+
#
|
|
13
|
+
# @param registry [Runtime::Registry] the booted registry to read repositories from
|
|
14
|
+
# @param domain [String] the owning chapter's name
|
|
15
|
+
# @param fields [Array<Forms::Field>] the resolved field tree to search
|
|
16
|
+
# @return [Hash{String => Array<Array(String, String)>, nil}] one entry per
|
|
17
|
+
# `:reference` field found, keyed by its `path`; `nil` when the field's target
|
|
18
|
+
# could not be resolved or its options could not be read
|
|
10
19
|
def self.collect(registry, domain, fields)
|
|
11
20
|
targets = {}
|
|
12
21
|
walk(fields) { |field| targets[field.path] = field.target_aggregate if field.kind == :reference }
|
|
13
22
|
targets.transform_values { |aggregate| aggregate && options_for(registry, domain, aggregate) }
|
|
14
23
|
end
|
|
15
24
|
|
|
25
|
+
# Visits every field in a tree, depth-first, including each `:list`/`:group`
|
|
26
|
+
# field's own children.
|
|
27
|
+
#
|
|
28
|
+
# @param fields [Array<Forms::Field>] the field tree to walk
|
|
29
|
+
# @yieldparam field [Forms::Field] each field visited, parent before children
|
|
30
|
+
# @return [void]
|
|
16
31
|
def self.walk(fields, &block)
|
|
17
32
|
fields.each do |field|
|
|
18
33
|
block.call(field)
|
|
@@ -20,6 +35,15 @@ module Hecks
|
|
|
20
35
|
end
|
|
21
36
|
end
|
|
22
37
|
|
|
38
|
+
# Reads up to 200 records' ids as `<select>` options, for one reference field's
|
|
39
|
+
# target aggregate.
|
|
40
|
+
#
|
|
41
|
+
# @param registry [Runtime::Registry] the booted registry to read the repository from
|
|
42
|
+
# @param domain [String] the owning chapter's name
|
|
43
|
+
# @param aggregate [Bluebook::Aggregate] the referenced aggregate to list
|
|
44
|
+
# @return [Array<Array(String, String)>, nil] up to 200 `[id, id]` pairs; `nil` when
|
|
45
|
+
# the repository cannot be resolved (a wiring gap degrades to a plain text id
|
|
46
|
+
# input, not a 500)
|
|
23
47
|
def self.options_for(registry, domain, aggregate)
|
|
24
48
|
registry.repository(domain, aggregate).all.first(200).map { |instance| [instance.id, instance.id] }
|
|
25
49
|
rescue StandardError
|
|
@@ -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
|
|
@@ -18,15 +18,21 @@ module Hecks
|
|
|
18
18
|
# every money-shaped VO in the corpus spells it exactly this way,
|
|
19
19
|
# the two-attribute convention the language itself never enforces
|
|
20
20
|
# but every real chapter follows.
|
|
21
|
+
# @param value_object [Bluebook::ValueObject] the value object to classify
|
|
22
|
+
# @return [Boolean] true if its attributes are exactly `cents` and `currency`
|
|
21
23
|
def money?(value_object)
|
|
22
24
|
value_object.attributes.map { |a| a.name.to_s }.sort == %w[cents currency]
|
|
23
25
|
end
|
|
24
26
|
|
|
25
|
-
# A VO with exactly one attribute is a
|
|
27
|
+
# A VO with exactly one attribute is a name for a scalar, not a
|
|
26
28
|
# genuine group (EmailAddress{address}, CustomerNumber{value}) —
|
|
27
29
|
# [[feedback_name_the_scalar_field]]'s own reasoning, shared here
|
|
28
30
|
# rather than re-decided per caller. Returns the sole attribute,
|
|
29
31
|
# or nil for anything else.
|
|
32
|
+
#
|
|
33
|
+
# @param value_object [Bluebook::ValueObject] the value object to inspect
|
|
34
|
+
# @return [Bluebook::Attribute, nil] its one declared attribute, or nil if
|
|
35
|
+
# it declares zero or more than one
|
|
30
36
|
def sole_attribute(value_object)
|
|
31
37
|
return nil unless value_object.attributes.size == 1
|
|
32
38
|
|
|
@@ -38,6 +44,10 @@ module Hecks
|
|
|
38
44
|
# money-shaped (money's own two members are handled by `money?`
|
|
39
45
|
# instead, since which one governs ordering is a money-specific
|
|
40
46
|
# decision, not a general "pick the first number" one).
|
|
47
|
+
#
|
|
48
|
+
# @param value_object [Bluebook::ValueObject] the value object to inspect
|
|
49
|
+
# @return [Bluebook::Attribute, nil] its first Integer- or Float-typed
|
|
50
|
+
# attribute, or nil if it declares none
|
|
41
51
|
def numeric_member(value_object)
|
|
42
52
|
value_object.attributes.find { |a| %w[Integer Float].include?(a.type.to_s) }
|
|
43
53
|
end
|
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
|
@@ -8,20 +8,52 @@ module Hecks
|
|
|
8
8
|
|
|
9
9
|
attr_reader :realm, :domain, :version, :aggregate, :verb, :kind
|
|
10
10
|
|
|
11
|
+
# Builds the FQN of one command on an aggregate.
|
|
12
|
+
#
|
|
13
|
+
# @param realm [String, nil] deployment identity from the world, or nil for an
|
|
14
|
+
# unrealmed address
|
|
15
|
+
# @param domain [String] the domain name
|
|
16
|
+
# @param aggregate [String] the aggregate name the command belongs to
|
|
17
|
+
# @param command [String] the command's PascalCase verb
|
|
18
|
+
# @param version [String, nil] the pinned domain version, or nil for the
|
|
19
|
+
# world's configured latest alias
|
|
20
|
+
# @return [Fqn] the command address
|
|
21
|
+
# @raise [Fqn::Invalid] if any segment is empty or contains a separator, or if
|
|
22
|
+
# `command` is not a valid PascalCase command name
|
|
11
23
|
def self.command(realm:, domain:, aggregate:, command:, version: nil)
|
|
12
24
|
new(realm: realm, domain: domain, version: version, aggregate: aggregate, verb: command, kind: :command)
|
|
13
25
|
end
|
|
14
26
|
|
|
27
|
+
# Builds the FQN of one query, either on an aggregate or domain-level.
|
|
28
|
+
#
|
|
29
|
+
# @param realm [String, nil] deployment identity from the world, or nil for an
|
|
30
|
+
# unrealmed address
|
|
31
|
+
# @param domain [String] the domain name
|
|
32
|
+
# @param query [String] the query's snake_case verb
|
|
33
|
+
# @param aggregate [String, nil] the aggregate the query belongs to, or nil for
|
|
34
|
+
# a domain-level read model
|
|
35
|
+
# @param version [String, nil] the pinned domain version, or nil for the
|
|
36
|
+
# world's configured latest alias
|
|
37
|
+
# @return [Fqn] the query address
|
|
38
|
+
# @raise [Fqn::Invalid] if any segment is empty or contains a separator, or if
|
|
39
|
+
# `query` is not a valid snake_case query name
|
|
15
40
|
def self.query(realm:, domain:, query:, aggregate: nil, version: nil)
|
|
16
41
|
new(realm: realm, domain: domain, version: version, aggregate: aggregate, verb: query, kind: :query)
|
|
17
42
|
end
|
|
18
43
|
|
|
19
|
-
#
|
|
44
|
+
# One order-dependent parse pipeline: split -> shape-validate -> dispatch
|
|
20
45
|
# on segment count -> split domain/version -> classify kind -> cross-
|
|
21
46
|
# field validate -> construct. Each step consumes locals (segments, verb,
|
|
22
47
|
# kind) the step before it derived; splitting would mean threading all of
|
|
23
48
|
# them back out as parameters/returns between new methods, for no
|
|
24
49
|
# readability gain over reading the pipeline top to bottom once.
|
|
50
|
+
#
|
|
51
|
+
# @param text [String] a `Realm::Domain::Aggregate.verb` address, with domain
|
|
52
|
+
# optionally `@version`-pinned and aggregate optional for a domain-level query
|
|
53
|
+
# @return [Fqn] the parsed address
|
|
54
|
+
# @raise [Fqn::Invalid] if `text` is not shaped like a FQN, its verb is neither
|
|
55
|
+
# PascalCase nor snake_case, its domain version is malformed, or it names a
|
|
56
|
+
# domain-level command
|
|
25
57
|
# rubocop:disable-next Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
26
58
|
def self.parse(text)
|
|
27
59
|
head, separator, verb = text.to_s.rpartition(".")
|
|
@@ -53,9 +85,28 @@ module Hecks
|
|
|
53
85
|
new(realm: realm, domain: domain, version: version, aggregate: aggregate, verb: verb, kind: kind)
|
|
54
86
|
end
|
|
55
87
|
|
|
88
|
+
# Whether `name` is a valid command verb: PascalCase.
|
|
89
|
+
#
|
|
90
|
+
# @param name [String, Symbol, #to_s] the candidate verb
|
|
91
|
+
# @return [Boolean] true if `name` matches the PascalCase command shape
|
|
56
92
|
def self.command_name?(name) = /\A[A-Z][A-Za-z0-9]*\z/.match?(name.to_s)
|
|
93
|
+
|
|
94
|
+
# Whether `name` is a valid query verb: snake_case.
|
|
95
|
+
#
|
|
96
|
+
# @param name [String, Symbol, #to_s] the candidate verb
|
|
97
|
+
# @return [Boolean] true if `name` matches the snake_case query shape
|
|
57
98
|
def self.query_name?(name) = /\A[a-z][a-z0-9_]*\z/.match?(name.to_s)
|
|
58
99
|
|
|
100
|
+
# @param realm [String, nil] deployment identity from the world, or nil for an
|
|
101
|
+
# unrealmed address
|
|
102
|
+
# @param domain [String] the domain name
|
|
103
|
+
# @param aggregate [String, nil] the aggregate name, or nil for a domain-level query
|
|
104
|
+
# @param verb [String] the command or query verb
|
|
105
|
+
# @param kind [Symbol, String] `:command` or `:query`
|
|
106
|
+
# @param version [String, nil] the pinned domain version, or nil for the
|
|
107
|
+
# world's configured latest alias
|
|
108
|
+
# @raise [Fqn::Invalid] if any segment is empty or contains a separator, or if
|
|
109
|
+
# `verb` does not match the shape required by `kind`
|
|
59
110
|
def initialize(realm:, domain:, aggregate:, verb:, kind:, version: nil)
|
|
60
111
|
@realm = realm && segment(realm, "realm")
|
|
61
112
|
@domain = segment(domain, "domain")
|
|
@@ -68,7 +119,14 @@ module Hecks
|
|
|
68
119
|
raise Invalid, "#{@kind} FQN has an invalid verb #{@verb.inspect}" unless valid
|
|
69
120
|
end
|
|
70
121
|
|
|
122
|
+
# Whether this address names a command.
|
|
123
|
+
#
|
|
124
|
+
# @return [Boolean] true if this address's kind is `:command`
|
|
71
125
|
def command? = @kind == :command
|
|
126
|
+
|
|
127
|
+
# Whether this address names a query.
|
|
128
|
+
#
|
|
129
|
+
# @return [Boolean] true if this address's kind is `:query`
|
|
72
130
|
def query? = @kind == :query
|
|
73
131
|
|
|
74
132
|
def to_s
|
|
@@ -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
|
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
Hecks.bluebook "Privacy" do
|
|
2
|
+
vision "Which fields of a domain's own aggregates are sensitive is a fact about that domain, declared apart from it — the domain itself never spells out that one of its own attributes is PII or PHI."
|
|
3
|
+
supporting
|
|
4
|
+
|
|
5
|
+
# A DOMAIN ATTACHING THIS NEVER MARKS ITSELF — `Mark`'s `role
|
|
6
|
+
# "Deployment"` matches every other wiring-time command in this
|
|
7
|
+
# codebase (`Hecksagon::UsesFramework`, `Tenancy::Tenant.Register`):
|
|
8
|
+
# something outside the domain's own bluebook decides a field is
|
|
9
|
+
# sensitive, the same way persistence binds are a wiring decision
|
|
10
|
+
# and not baked into an aggregate's own declaration.
|
|
11
|
+
aggregate "Marking" do
|
|
12
|
+
description "One attribute of one domain's own aggregate, flagged sensitive — and the role required to read it unredacted."
|
|
13
|
+
|
|
14
|
+
attribute :domain, MarkingDomain
|
|
15
|
+
attribute :attribute_path, AttributePath
|
|
16
|
+
attribute :category, Category
|
|
17
|
+
attribute :readable_by, RoleName
|
|
18
|
+
|
|
19
|
+
identified_by :domain, :attribute_path
|
|
20
|
+
|
|
21
|
+
value_object "MarkingDomain" do
|
|
22
|
+
attribute :value, String
|
|
23
|
+
invariant("a marking names the domain it applies to") { !value.to_s.empty? }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# DOTTED, same convention Forms' own `Field#path` and a query's
|
|
27
|
+
# cross-object `where` clause already use — `"attendee.medications"`,
|
|
28
|
+
# not a bare leaf name, so a marking can reach into a nested value
|
|
29
|
+
# object rather than only a top-level attribute.
|
|
30
|
+
value_object "AttributePath" do
|
|
31
|
+
attribute :value, String
|
|
32
|
+
invariant("a marking names an attribute path") { !value.to_s.empty? }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# OPEN-ENDED ON PURPOSE — "pii", "phi", whatever else a consuming
|
|
36
|
+
# domain finds itself needing, none of it enumerated here. This
|
|
37
|
+
# bluebook is the mechanism (mark, gate, redact); it does not also
|
|
38
|
+
# own the vocabulary of sensitivity categories that exist in the
|
|
39
|
+
# world.
|
|
40
|
+
value_object "Category" do
|
|
41
|
+
attribute :value, String
|
|
42
|
+
invariant("a marking is categorized") { !value.to_s.empty? }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
value_object "RoleName" do
|
|
46
|
+
attribute :value, String
|
|
47
|
+
invariant("reading a marked field unredacted requires a named role") { !value.to_s.empty? }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
command "Mark" do
|
|
51
|
+
role "Deployment"
|
|
52
|
+
goal "Flag one attribute of a domain's own aggregate as sensitive, gating who may read it unredacted"
|
|
53
|
+
|
|
54
|
+
attribute :domain, MarkingDomain
|
|
55
|
+
attribute :attribute_path, AttributePath
|
|
56
|
+
attribute :category, Category
|
|
57
|
+
attribute :readable_by, RoleName
|
|
58
|
+
|
|
59
|
+
emits Marked
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
command "Unmark" do
|
|
63
|
+
role "Deployment"
|
|
64
|
+
goal "Remove a sensitivity flag from an attribute"
|
|
65
|
+
|
|
66
|
+
reference_to Marking
|
|
67
|
+
|
|
68
|
+
emits Unmarked
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
query "ForDomain" do
|
|
72
|
+
description "Every marking declared for one domain, read by whatever gates or redacts that domain's own records."
|
|
73
|
+
attribute :domain, MarkingDomain
|
|
74
|
+
where(domain: :domain)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# **Cryptoshredding** — a right-to-erasure request satisfied without
|
|
79
|
+
# rewriting or deleting a single event. `key_reference` is an opaque
|
|
80
|
+
# handle a caller obtains from `Ports::KeyVault.issue` before
|
|
81
|
+
# dispatching `Issue`, the same "nothing is minted inside a bluebook"
|
|
82
|
+
# convention `Identity::Register`'s own `identity_id` and
|
|
83
|
+
# `Delivery::Record`'s own `message_id` already hold to (the
|
|
84
|
+
# expression sublanguage cannot call a port at all). The key material
|
|
85
|
+
# itself never appears here or on any emitted event — only the fact
|
|
86
|
+
# that a reference exists, and the fact that it was destroyed.
|
|
87
|
+
aggregate "SubjectKey" do
|
|
88
|
+
description "One data subject's own encryption key, from the moment it exists to the moment it is destroyed."
|
|
89
|
+
|
|
90
|
+
attribute :domain, KeyDomain
|
|
91
|
+
attribute :subject_id, SubjectId
|
|
92
|
+
attribute :key_reference, KeyReference
|
|
93
|
+
|
|
94
|
+
identified_by :domain, :subject_id
|
|
95
|
+
|
|
96
|
+
value_object "KeyDomain" do
|
|
97
|
+
attribute :value, String
|
|
98
|
+
invariant("a key names the domain its subject belongs to") { !value.to_s.empty? }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
value_object "SubjectId" do
|
|
102
|
+
attribute :value, String
|
|
103
|
+
invariant("a key names the subject it was issued for") { !value.to_s.empty? }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
value_object "KeyReference" do
|
|
107
|
+
attribute :value, String
|
|
108
|
+
invariant("a key carries the vault's own opaque reference to it") { !value.to_s.empty? }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# `Shred`'s own guard against running twice — the same mechanism
|
|
112
|
+
# `Compliance::PrivacyReview`'s `Clear`/`Escalate` already use rather
|
|
113
|
+
# than an optional timestamp plus a hand-written `given`.
|
|
114
|
+
lifecycle :status, default: "active" do
|
|
115
|
+
transition "Shred" => "shredded", from: "active"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
command "Issue" do
|
|
119
|
+
role "System"
|
|
120
|
+
goal "Record that a subject's data is now encrypted under a fresh, dedicated key"
|
|
121
|
+
|
|
122
|
+
attribute :domain, KeyDomain
|
|
123
|
+
attribute :subject_id, SubjectId
|
|
124
|
+
attribute :key_reference, KeyReference
|
|
125
|
+
|
|
126
|
+
sets :domain
|
|
127
|
+
sets :subject_id
|
|
128
|
+
sets :key_reference
|
|
129
|
+
|
|
130
|
+
emits SubjectKeyIssued
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# **Records destruction, never performs it** — the actual
|
|
134
|
+
# `Ports::KeyVault.destroy` call happens in driving-side code before
|
|
135
|
+
# this is ever dispatched, the same boundary `send_issue.rb` draws
|
|
136
|
+
# around `Delivery::Record` for an already-sent email. This
|
|
137
|
+
# command's own job is to make an already-irreversible fact
|
|
138
|
+
# durable, not to cause it.
|
|
139
|
+
command "Shred" do
|
|
140
|
+
role "Privacy officer"
|
|
141
|
+
goal "Mark this subject's key destroyed, so its ciphertext is recorded as permanently unrecoverable"
|
|
142
|
+
|
|
143
|
+
reference_to SubjectKey
|
|
144
|
+
|
|
145
|
+
emits SubjectKeyShredded
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
query "ForSubject" do
|
|
149
|
+
description "One subject's key record, checked before any attempt to decrypt their sensitive fields."
|
|
150
|
+
attribute :domain, KeyDomain
|
|
151
|
+
attribute :subject_id, SubjectId
|
|
152
|
+
where(domain: :domain, subject_id: :subject_id)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
@@ -30,6 +30,21 @@
|
|
|
30
30
|
"scope": "compliance:box_surrender_review.open",
|
|
31
31
|
"verb": "Compliance::BoxSurrenderReview.Open",
|
|
32
32
|
"role": "System"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"scope": "compliance:privacy_review.clear",
|
|
36
|
+
"verb": "Compliance::PrivacyReview.Clear",
|
|
37
|
+
"role": "Compliance officer"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"scope": "compliance:privacy_review.escalate",
|
|
41
|
+
"verb": "Compliance::PrivacyReview.Escalate",
|
|
42
|
+
"role": "Compliance officer"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"scope": "compliance:privacy_review.open",
|
|
46
|
+
"verb": "Compliance::PrivacyReview.Open",
|
|
47
|
+
"role": "System"
|
|
33
48
|
}
|
|
34
49
|
],
|
|
35
50
|
"roles": [
|