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
|
@@ -2,30 +2,34 @@ require_relative "../projector"
|
|
|
2
2
|
|
|
3
3
|
module Hecks
|
|
4
4
|
module Projections
|
|
5
|
-
# A
|
|
5
|
+
# A domain's own shape, projected as Mermaid diagrams — the same
|
|
6
6
|
# trick `Projections::Reference`/`DocsProjector` already play for
|
|
7
|
-
# prose, one level further: a diagram generated
|
|
7
|
+
# prose, one level further: a diagram generated from the
|
|
8
8
|
# declaration can't drift from it the way a hand-drawn one
|
|
9
9
|
# inevitably does, because there is no second copy to forget to
|
|
10
10
|
# update.
|
|
11
11
|
#
|
|
12
|
-
#
|
|
12
|
+
# ## Why Mermaid, not Graphviz
|
|
13
|
+
#
|
|
14
|
+
# Mermaid, not Graphviz (the two considered) — every diagram type
|
|
13
15
|
# below has a Mermaid form purpose-built for exactly what the
|
|
14
|
-
# underlying construct already is (a `lifecycle`
|
|
16
|
+
# underlying construct already is (a `lifecycle` is a state
|
|
15
17
|
# machine, `has_many`/`belongs_to` already speaks in cardinality,
|
|
16
|
-
# `emits`/`trigger` already
|
|
18
|
+
# `emits`/`trigger` already is a directed graph), and the output is
|
|
17
19
|
# plain text that renders natively wherever this project's own docs
|
|
18
20
|
# already live — GitHub markdown, this repo's generated docs, Claude
|
|
19
21
|
# Artifacts — with no build step and no external binary. Graphviz's
|
|
20
|
-
#
|
|
22
|
+
# dot format needs an actual render step (a `dot` binary, or a WASM
|
|
21
23
|
# port) to become anything viewable, which is a real dependency this
|
|
22
24
|
# repository's own discipline (see rust/parser's Cargo.toml: "no
|
|
23
25
|
# dependency earns its way past std") would rather not take just to
|
|
24
26
|
# draw a diagram.
|
|
25
27
|
#
|
|
26
|
-
#
|
|
28
|
+
# ## Diagram kinds
|
|
29
|
+
#
|
|
30
|
+
# Four diagram kinds, one file each per domain except lifecycles
|
|
27
31
|
# (one per lifecycle-bearing construct, since that's how a reader
|
|
28
|
-
# actually reaches for it — looking at
|
|
32
|
+
# actually reaches for it — looking at one aggregate's states, not
|
|
29
33
|
# every aggregate's at once):
|
|
30
34
|
#
|
|
31
35
|
# <Name>_lifecycle.mmd stateDiagram-v2 one per lifecycle
|
|
@@ -47,27 +51,29 @@ module Hecks
|
|
|
47
51
|
# <Name>_surface.mmd flowchart one per aggregate/entity that
|
|
48
52
|
# declares at least one command
|
|
49
53
|
# or query — everything you can
|
|
50
|
-
#
|
|
51
|
-
#
|
|
54
|
+
# do to it and ask about it,
|
|
55
|
+
# and what each command writes,
|
|
52
56
|
# in one place
|
|
53
57
|
# <Name>_saga.mmd stateDiagram-v2 one per process_manager —
|
|
54
58
|
# its own states, and what
|
|
55
59
|
# each transition dispatches
|
|
56
60
|
# elsewhere in the domain
|
|
57
|
-
# frameworks.mmd flowchart every
|
|
61
|
+
# frameworks.mmd flowchart every other domain this one
|
|
58
62
|
# depends on — a shared
|
|
59
63
|
# framework it `uses_framework`,
|
|
60
64
|
# or a domain a policy reaches
|
|
61
65
|
# `across` — the one diagram
|
|
62
|
-
# here that looks
|
|
66
|
+
# here that looks outward past
|
|
63
67
|
# this domain's own boundary
|
|
64
68
|
#
|
|
65
|
-
#
|
|
66
|
-
#
|
|
69
|
+
# ## Node and construct naming
|
|
70
|
+
#
|
|
71
|
+
# Construct names (aggregate/entity/command/event) are used bare,
|
|
72
|
+
# unsanitized, as Mermaid node/entity ids — safe because this
|
|
67
73
|
# language's own word grammar only ever admits simple CamelCase/
|
|
68
74
|
# snake_case identifiers there (confirmed: no space or punctuation
|
|
69
75
|
# appears in any real aggregate/command/event name across the corpus
|
|
70
|
-
# this projects from). A `role:`
|
|
76
|
+
# this projects from). A `role:` string is free text, though — the
|
|
71
77
|
# real corpus already has "Back office"/"Vault officer"/"Branch
|
|
72
78
|
# clerk" — so `roles.mmd` is the one diagram here that sanitizes a
|
|
73
79
|
# name into an id (`role_id`) while keeping the real string as the
|
|
@@ -79,6 +85,14 @@ module Hecks
|
|
|
79
85
|
|
|
80
86
|
module_function
|
|
81
87
|
|
|
88
|
+
# Projects one domain's whole shape into every diagram kind it has data
|
|
89
|
+
# for, one Mermaid file per entry.
|
|
90
|
+
#
|
|
91
|
+
# @param bluebook [Bluebook::Chapter] the assembled chapter to project
|
|
92
|
+
# @param options [Hash{Symbol => Object}] projection options; only `:hecksagon`
|
|
93
|
+
# (a `Bluebook::Hecksagon`, or `nil`) is read, and only by `frameworks_diagram`
|
|
94
|
+
# @return [Hash{String => String}] each diagram's filename (such as
|
|
95
|
+
# `"relationships.mmd"`) mapped to its rendered Mermaid source
|
|
82
96
|
def call(bluebook:, options: {})
|
|
83
97
|
files = {}
|
|
84
98
|
|
|
@@ -125,25 +139,46 @@ module Hecks
|
|
|
125
139
|
|
|
126
140
|
# ── shared ────────────────────────────────────────────────────────
|
|
127
141
|
|
|
128
|
-
#
|
|
129
|
-
#
|
|
142
|
+
# Every aggregate and entity in this domain, flattened into one list —
|
|
143
|
+
# everything a lifecycle/relationship/command diagram walks alike.
|
|
144
|
+
#
|
|
145
|
+
# An entity can carry its own lifecycle, relationship, or command
|
|
146
|
+
# too — its own `lifecycle`/`reference_to`/`command` block,
|
|
130
147
|
# addressed through its holding aggregate the same way
|
|
131
148
|
# `DocsProjector` already treats an aggregate and its entities
|
|
132
149
|
# alike. Walking both here means a domain's entity gaining any of
|
|
133
150
|
# these needs no change to this file.
|
|
151
|
+
#
|
|
152
|
+
# @param bluebook [Bluebook::Chapter] the chapter to walk
|
|
153
|
+
# @return [Array<Bluebook::Aggregate, Class>] every aggregate, and every entity
|
|
154
|
+
# class (a `Bluebook::Entity` subclass) nested under it, in declaration order
|
|
134
155
|
def holders(bluebook)
|
|
135
156
|
bluebook.aggregates.flat_map { |aggregate| [aggregate, *aggregate.entities] }
|
|
136
157
|
end
|
|
137
158
|
|
|
159
|
+
# The holders (see `holders`) that declare their own `lifecycle` block —
|
|
160
|
+
# one diagram file is projected per lifecycle-bearing holder, not one
|
|
161
|
+
# per domain.
|
|
162
|
+
#
|
|
163
|
+
# @param bluebook [Bluebook::Chapter] the chapter to walk
|
|
164
|
+
# @return [Array<Bluebook::Aggregate, Class>] every holder with a non-nil
|
|
165
|
+
# `lifecycle`
|
|
138
166
|
def holders_with_lifecycle(bluebook) = holders(bluebook).select(&:lifecycle)
|
|
139
167
|
|
|
140
|
-
#
|
|
168
|
+
# Renders the two-line `%%` comment banner every generated diagram file
|
|
169
|
+
# opens with, warning against hand edits and naming how to regenerate it.
|
|
170
|
+
#
|
|
171
|
+
# `chapter_name` drives the re-run hint always — that's the one
|
|
141
172
|
# argument `bin/project_diagrams` actually takes, regardless of
|
|
142
173
|
# which single aggregate/entity `subject` happens to name. Passing
|
|
143
174
|
# the wrong one here once already produced a real, committed
|
|
144
175
|
# `Order_lifecycle.mmd` telling a reader to run
|
|
145
176
|
# `bin/project_diagrams <domain-path> Order` — a chapter name
|
|
146
177
|
# Hecks.boot has never heard of.
|
|
178
|
+
#
|
|
179
|
+
# @param chapter_name [String] the domain's chapter name, used in the re-run hint
|
|
180
|
+
# @param subject [String] prose naming what this specific file was generated from
|
|
181
|
+
# @return [String] the two-line `%%`-commented Mermaid banner, newline-terminated
|
|
147
182
|
def header(chapter_name, subject)
|
|
148
183
|
<<~HEADER
|
|
149
184
|
%% GENERATED by bin/project_diagrams from #{subject} — DO NOT EDIT BY HAND.
|
|
@@ -153,6 +188,15 @@ module Hecks
|
|
|
153
188
|
|
|
154
189
|
# ── lifecycle -> stateDiagram-v2 ─────────────────────────────────
|
|
155
190
|
|
|
191
|
+
# Renders one holder's own declared `lifecycle` as a Mermaid
|
|
192
|
+
# `stateDiagram-v2` — its default starting state, and one edge per
|
|
193
|
+
# transition, labeled by the command that causes it.
|
|
194
|
+
#
|
|
195
|
+
# @param bluebook [Bluebook::Chapter] the chapter `holder` belongs to, for the
|
|
196
|
+
# file's re-run banner
|
|
197
|
+
# @param holder [Bluebook::Aggregate, Class] the aggregate or entity class (a
|
|
198
|
+
# `Bluebook::Entity` subclass) whose `lifecycle` to draw
|
|
199
|
+
# @return [String] the rendered Mermaid `stateDiagram-v2` source
|
|
156
200
|
def lifecycle_diagram(bluebook, holder)
|
|
157
201
|
lifecycle = holder.lifecycle
|
|
158
202
|
edges = lifecycle.transitions.flat_map do |command_name, transition|
|
|
@@ -169,18 +213,26 @@ module Hecks
|
|
|
169
213
|
|
|
170
214
|
# ── relationships -> erDiagram ───────────────────────────────────
|
|
171
215
|
|
|
172
|
-
#
|
|
216
|
+
# Renders the whole domain's declared `has_many`/`has_one`/`belongs_to`/
|
|
217
|
+
# `reference_to` attributes as a Mermaid `erDiagram`, one edge per
|
|
218
|
+
# reference attribute across every holder.
|
|
219
|
+
#
|
|
220
|
+
# Standard crow's-foot reading, the same convention every ORM's own
|
|
173
221
|
# ERD generator (Rails' erd gem included) already uses:
|
|
174
|
-
# `has_many`/`has_one` are read from the
|
|
222
|
+
# `has_many`/`has_one` are read from the owning side — one Holder
|
|
175
223
|
# relates to many/one Target. `belongs_to`/`reference_to` are read
|
|
176
|
-
# from the
|
|
177
|
-
#
|
|
224
|
+
# from the target's side instead — one Target can be pointed at by
|
|
225
|
+
# many Holders — because a bare reference carries no promise about
|
|
178
226
|
# how many holders point back at it; "many" is the honest default
|
|
179
227
|
# absent a declared uniqueness rule this language doesn't expose.
|
|
180
228
|
# `optional?` only ever softens the side that can genuinely be
|
|
181
229
|
# absent (a nilable reference, an empty has_one) — never the
|
|
182
230
|
# crow's-foot "many" marker, which is a structural fact independent
|
|
183
231
|
# of any one instance's optionality.
|
|
232
|
+
#
|
|
233
|
+
# @param bluebook [Bluebook::Chapter] the chapter to project
|
|
234
|
+
# @return [String, nil] the rendered Mermaid `erDiagram` source, or `nil` if the
|
|
235
|
+
# domain declares no reference attribute at all
|
|
184
236
|
def relationship_diagram(bluebook)
|
|
185
237
|
edges = holders(bluebook).flat_map do |holder|
|
|
186
238
|
holder.attributes.select(&:reference?).map { |attribute| relationship_edge(holder, attribute) }
|
|
@@ -191,6 +243,14 @@ module Hecks
|
|
|
191
243
|
"#{header(bluebook.name, subject)}erDiagram\n#{edges.join("\n")}\n"
|
|
192
244
|
end
|
|
193
245
|
|
|
246
|
+
# One erDiagram edge for one holder's own declared reference attribute,
|
|
247
|
+
# in the DSL word's own crow's-foot reading (see `relationship_diagram`).
|
|
248
|
+
#
|
|
249
|
+
# @param holder [Bluebook::Aggregate, Class] the aggregate or entity class (a
|
|
250
|
+
# `Bluebook::Entity` subclass) that declares `attribute`
|
|
251
|
+
# @param attribute [Bluebook::Attribute] a reference attribute (`attribute.reference?`
|
|
252
|
+
# is true); its `relationship` names which DSL word minted it
|
|
253
|
+
# @return [String] the rendered `erDiagram` edge line
|
|
194
254
|
def relationship_edge(holder, attribute)
|
|
195
255
|
target = attribute.type.target_name
|
|
196
256
|
case attribute.relationship
|
|
@@ -205,14 +265,18 @@ module Hecks
|
|
|
205
265
|
|
|
206
266
|
# ── dispatch -> flowchart ─────────────────────────────────────────
|
|
207
267
|
|
|
208
|
-
# A
|
|
209
|
-
#
|
|
268
|
+
# A command node, stadium-shaped (`(["..."])`); an event node,
|
|
269
|
+
# hexagonal (`{{"..."}}`) — one visual vocabulary for "a thing
|
|
210
270
|
# someone does" versus "a fact that happened", matching the
|
|
211
271
|
# language's own verb/event distinction. Command ids are qualified
|
|
212
272
|
# by their owning aggregate (`cmd_Order_Purchase`) since two
|
|
213
273
|
# aggregates may share a command name; event ids are bare
|
|
214
274
|
# (`evt_PizzaCreated`) since an event is this domain's own
|
|
215
275
|
# addressing key, the same way `policy.on_event` reaches it.
|
|
276
|
+
#
|
|
277
|
+
# @param bluebook [Bluebook::Chapter] the chapter to project
|
|
278
|
+
# @return [String, nil] the rendered Mermaid flowchart source, or `nil` if the
|
|
279
|
+
# domain declares no command `emits` and no policy at all
|
|
216
280
|
def dispatch_diagram(bluebook)
|
|
217
281
|
lines = []
|
|
218
282
|
|
|
@@ -231,21 +295,33 @@ module Hecks
|
|
|
231
295
|
"#{header(bluebook.name, subject)}flowchart LR\n#{lines.uniq.join("\n")}\n"
|
|
232
296
|
end
|
|
233
297
|
|
|
298
|
+
# One flowchart edge from a command to one event it declares it `emits`.
|
|
299
|
+
#
|
|
300
|
+
# @param holder [Bluebook::Aggregate, Class] the aggregate or entity class (a
|
|
301
|
+
# `Bluebook::Entity` subclass) that declares `command`
|
|
302
|
+
# @param command [Class] the command class (a `Bluebook::Command` subclass) that
|
|
303
|
+
# emits `event`
|
|
304
|
+
# @param event [String] the emitted event's name
|
|
305
|
+
# @return [String] the rendered `-->|emits|` edge line
|
|
234
306
|
def emits_edge(holder, command, event)
|
|
235
307
|
%( #{command_node(holder.hecks_name, command.hecks_name)} -->|emits| #{event_node(event)})
|
|
236
308
|
end
|
|
237
309
|
|
|
238
|
-
# `on_event`
|
|
239
|
-
# (`"Account.AccountFrozen"`)
|
|
310
|
+
# `on_event` is sometimes aggregate-qualified
|
|
311
|
+
# (`"Account.AccountFrozen"`) and sometimes bare
|
|
240
312
|
# (`"CustomerSuspended"`) in the real corpus — `emits` never is,
|
|
241
313
|
# so this always matches against the bare tail, the same
|
|
242
314
|
# normalization a reader has to do by eye today.
|
|
243
315
|
#
|
|
244
|
-
# A
|
|
316
|
+
# A trigger crossing into another domain (`policy.target_domain`)
|
|
245
317
|
# still draws — the target command just has no incoming `emits`
|
|
246
318
|
# edge of its own here, which honestly shows "dispatch continues
|
|
247
319
|
# elsewhere" rather than silently dropping the edge. The label
|
|
248
320
|
# names which domain, so that's not a dead end on the page either.
|
|
321
|
+
#
|
|
322
|
+
# @param policy [Bluebook::Policy] the policy to draw as a trigger edge
|
|
323
|
+
# @return [String] the rendered `-->|triggers|` (or `-->|triggers in <domain>|`)
|
|
324
|
+
# edge line
|
|
249
325
|
def trigger_edge(policy)
|
|
250
326
|
bare_event = policy.on_event.to_s.split(".").last
|
|
251
327
|
aggregate_name, command_name = policy.trigger_command.to_s.split(".", 2)
|
|
@@ -253,21 +329,39 @@ module Hecks
|
|
|
253
329
|
%( #{event_node(bare_event)} -->|#{label}| #{command_node(aggregate_name, command_name)})
|
|
254
330
|
end
|
|
255
331
|
|
|
332
|
+
# Mermaid stadium-shaped node id and label for one command, qualified by
|
|
333
|
+
# its owning aggregate since two aggregates may share a command name.
|
|
334
|
+
#
|
|
335
|
+
# @param aggregate_name [String] the command's owning aggregate/entity name
|
|
336
|
+
# @param command_name [String] the command's own name
|
|
337
|
+
# @return [String] the rendered `cmd_<aggregate>_<command>(["..."])` node
|
|
256
338
|
def command_node(aggregate_name, command_name)
|
|
257
339
|
%(cmd_#{aggregate_name}_#{command_name}(["#{aggregate_name}.#{command_name}"]))
|
|
258
340
|
end
|
|
259
341
|
|
|
342
|
+
# Mermaid hexagonal node id and label for one event, addressed bare since
|
|
343
|
+
# an event is this domain's own addressing key.
|
|
344
|
+
#
|
|
345
|
+
# @param event_name [String] the event's own name
|
|
346
|
+
# @return [String] the rendered `evt_<event>{{"..."}}` node
|
|
260
347
|
def event_node(event_name) = %(evt_#{event_name}{{"#{event_name}"}})
|
|
261
348
|
|
|
262
349
|
# ── roles -> flowchart ────────────────────────────────────────────
|
|
263
350
|
|
|
264
|
-
#
|
|
351
|
+
# Renders every command's declared `role` as a Mermaid flowchart, one
|
|
352
|
+
# edge per role issuing a command.
|
|
353
|
+
#
|
|
354
|
+
# Who issues what, across the whole domain — data no existing
|
|
265
355
|
# projection draws at all today (the reference pages' own
|
|
266
356
|
# `command_entry` only ever prints a command's role as a single
|
|
267
357
|
# line of prose, never assembled across commands). A command with
|
|
268
358
|
# no declared `role` draws nothing — there is no fact to state.
|
|
269
359
|
# Circle-shaped so a role reads as "who" beside `dispatch.mmd`'s
|
|
270
360
|
# stadium ("what someone does") and hexagon ("what happened").
|
|
361
|
+
#
|
|
362
|
+
# @param bluebook [Bluebook::Chapter] the chapter to project
|
|
363
|
+
# @return [String, nil] the rendered Mermaid flowchart source, or `nil` if no
|
|
364
|
+
# command in the domain declares a `role`
|
|
271
365
|
def roles_diagram(bluebook)
|
|
272
366
|
lines = holders(bluebook).flat_map do |holder|
|
|
273
367
|
holder.commands.select(&:role).map { |command| role_edge(holder, command) }
|
|
@@ -278,22 +372,41 @@ module Hecks
|
|
|
278
372
|
"#{header(bluebook.name, subject)}flowchart LR\n#{lines.uniq.join("\n")}\n"
|
|
279
373
|
end
|
|
280
374
|
|
|
375
|
+
# One flowchart edge from a command's declared role to the command
|
|
376
|
+
# itself.
|
|
377
|
+
#
|
|
378
|
+
# @param holder [Bluebook::Aggregate, Class] the aggregate or entity class (a
|
|
379
|
+
# `Bluebook::Entity` subclass) that declares `command`
|
|
380
|
+
# @param command [Class] the command class (a `Bluebook::Command` subclass); its
|
|
381
|
+
# `role` is drawn
|
|
382
|
+
# @return [String] the rendered `-->|issues|` edge line
|
|
281
383
|
def role_edge(holder, command)
|
|
282
384
|
%( #{role_node(command.role)} -->|issues| #{command_node(holder.hecks_name, command.hecks_name)})
|
|
283
385
|
end
|
|
284
386
|
|
|
387
|
+
# Mermaid circular node id and label for one role, keeping the real
|
|
388
|
+
# free-text role string as the displayed label (see `role_id`).
|
|
389
|
+
#
|
|
390
|
+
# @param role_name [String] the command's declared `role` text
|
|
391
|
+
# @return [String] the rendered `role_<id>((<role>))` node
|
|
285
392
|
def role_node(role_name) = %(#{role_id(role_name)}((#{role_name})))
|
|
286
393
|
|
|
287
|
-
#
|
|
394
|
+
# Sanitizes a free-text role name into a legal Mermaid node id.
|
|
395
|
+
#
|
|
396
|
+
# A role name is free text ("Back office", "Vault officer") —
|
|
288
397
|
# unlike every other name this file uses as a bare id, this one
|
|
289
398
|
# has to be sanitized to become a legal Mermaid identifier. The
|
|
290
399
|
# real string still appears as the node's own label
|
|
291
400
|
# (`role_node`); only the id is mangled.
|
|
401
|
+
#
|
|
402
|
+
# @param role_name [String] the command's declared `role` text
|
|
403
|
+
# @return [String] `role_` followed by `role_name` with every run of
|
|
404
|
+
# non-alphanumeric characters collapsed to a single underscore
|
|
292
405
|
def role_id(role_name) = "role_#{role_name.to_s.gsub(/[^A-Za-z0-9]+/, '_')}"
|
|
293
406
|
|
|
294
407
|
# ── ports -> flowchart ───────────────────────────────────────────
|
|
295
408
|
|
|
296
|
-
# A
|
|
409
|
+
# A port operation is a boundary translation, not a verb or a fact —
|
|
297
410
|
# its own reference page says so plainly ("the builder behind it
|
|
298
411
|
# defines no `given` or `sets`, so an operation cannot read
|
|
299
412
|
# aggregate state or mutate a record itself"), so it gets a third
|
|
@@ -302,21 +415,25 @@ module Hecks
|
|
|
302
415
|
# state landing somewhere, the same reason a data store gets one
|
|
303
416
|
# in an ordinary flowchart.
|
|
304
417
|
#
|
|
305
|
-
#
|
|
418
|
+
# Two edge kinds per operation: a dotted "exposes" edge from the
|
|
306
419
|
# aggregate the port hangs off (always present — a port always
|
|
307
420
|
# belongs to exactly one aggregate), and a solid "to:" edge to
|
|
308
421
|
# whichever aggregate the operation itself names as its receiver
|
|
309
|
-
# (present only when `to:` is declared —
|
|
310
|
-
#
|
|
422
|
+
# (present only when `to:` is declared — an operation with no
|
|
423
|
+
# declared target has no destination of its own to draw).
|
|
311
424
|
# `emits` reuses `dispatch.mmd`'s own `event_node` unchanged — the
|
|
312
425
|
# same fact, reached from a different direction.
|
|
313
426
|
#
|
|
314
|
-
# `bluebook.aggregates`,
|
|
315
|
-
# lifecycle/relationship/command, a port belongs to an
|
|
427
|
+
# `bluebook.aggregates`, not the shared `holders` — unlike a
|
|
428
|
+
# lifecycle/relationship/command, a port belongs to an aggregate
|
|
316
429
|
# only; an entity has no `ports` method at all (confirmed: calling
|
|
317
430
|
# it raises, it isn't just always empty), so walking entities here
|
|
318
431
|
# the way every other diagram in this file does would crash on
|
|
319
432
|
# the first entity-bearing domain.
|
|
433
|
+
#
|
|
434
|
+
# @param bluebook [Bluebook::Chapter] the chapter to project
|
|
435
|
+
# @return [String, nil] the rendered Mermaid flowchart source, or `nil` if the
|
|
436
|
+
# domain declares no port operation at all
|
|
320
437
|
def ports_diagram(bluebook)
|
|
321
438
|
lines = bluebook.aggregates.flat_map do |holder|
|
|
322
439
|
holder.ports.flat_map { |port| port.operations.map { |operation| port_edges(holder, port, operation) } }
|
|
@@ -328,6 +445,14 @@ module Hecks
|
|
|
328
445
|
"#{header(bluebook.name, subject)}flowchart LR\n#{lines.uniq.join("\n")}\n"
|
|
329
446
|
end
|
|
330
447
|
|
|
448
|
+
# All of one port operation's own edges: the aggregate that exposes it,
|
|
449
|
+
# its optional `to:` routing target, and one edge per event it `emits`.
|
|
450
|
+
#
|
|
451
|
+
# @param holder [Bluebook::Aggregate] the aggregate `port` is attached to
|
|
452
|
+
# @param port [Bluebook::DomainPort] the port `operation` belongs to
|
|
453
|
+
# @param operation [Bluebook::PortOperation] the operation to draw
|
|
454
|
+
# @return [Array<String>] the rendered edge lines: one "exposes" edge, plus a
|
|
455
|
+
# "to:" edge when `operation.to` is declared, plus one "emits" edge per event
|
|
331
456
|
def port_edges(holder, port, operation)
|
|
332
457
|
op = port_operation_node(holder.hecks_name, port.name, operation.hecks_name)
|
|
333
458
|
edges = [" #{holder.hecks_name}[(#{holder.hecks_name})] -.->|exposes| #{op}"]
|
|
@@ -336,6 +461,13 @@ module Hecks
|
|
|
336
461
|
edges
|
|
337
462
|
end
|
|
338
463
|
|
|
464
|
+
# Mermaid trapezoidal node id and label for one port operation, qualified
|
|
465
|
+
# by both its owning aggregate and its owning port.
|
|
466
|
+
#
|
|
467
|
+
# @param aggregate_name [String] the operation's owning aggregate name
|
|
468
|
+
# @param port_name [String] the operation's owning port name
|
|
469
|
+
# @param operation_name [String] the operation's own name
|
|
470
|
+
# @return [String] the rendered `op_<aggregate>_<port>_<operation>[/"..."/]` node
|
|
339
471
|
def port_operation_node(aggregate_name, port_name, operation_name)
|
|
340
472
|
id = "op_#{aggregate_name}_#{port_name}_#{operation_name}"
|
|
341
473
|
%(#{id}[/"#{port_name}.#{operation_name}"/])
|
|
@@ -343,17 +475,20 @@ module Hecks
|
|
|
343
475
|
|
|
344
476
|
# ── read models -> flowchart ─────────────────────────────────────
|
|
345
477
|
|
|
346
|
-
#
|
|
347
|
-
#
|
|
478
|
+
# Renders the whole domain's declared `read_models` as a Mermaid
|
|
479
|
+
# flowchart, one edge per aggregate a read model is assembled from.
|
|
480
|
+
#
|
|
481
|
+
# The read-side complement to `relationships.mmd` — that diagram
|
|
482
|
+
# shows how aggregates reference each other for writes
|
|
348
483
|
# (`has_many`/`belongs_to`/`reference_to`); this shows how a
|
|
349
|
-
# `read_model`
|
|
484
|
+
# `read_model` assembles data for reads, from
|
|
350
485
|
# `aggregate_heads` — the same list `where`/`group_by`/`order_by`
|
|
351
486
|
# all operate over, and the one fact every read_model has
|
|
352
487
|
# regardless of whether it's rooted (`reference_target`) or
|
|
353
488
|
# gathers heads with no root at all (a rootless read model, real
|
|
354
489
|
# in the corpus: `AccountsByKind`).
|
|
355
490
|
#
|
|
356
|
-
# A
|
|
491
|
+
# A read model is a subroutine shape (`[[...]]`, "a predefined
|
|
357
492
|
# process") — a fourth shape, beside `ports.mmd`'s trapezoid and
|
|
358
493
|
# `dispatch.mmd`'s stadium/hexagon: not a verb, not a fact, not a
|
|
359
494
|
# boundary translation, but a standing, reusable view. Every
|
|
@@ -363,12 +498,16 @@ module Hecks
|
|
|
363
498
|
# (real in banking: `Account` feeds four) merges into one node
|
|
364
499
|
# across the whole diagram.
|
|
365
500
|
#
|
|
366
|
-
#
|
|
501
|
+
# The label names the shape of the answer, not just the name —
|
|
367
502
|
# `(count)`/`(median: field)` for the two real aggregations in the
|
|
368
503
|
# corpus, nothing appended for an ordinary row-returning
|
|
369
504
|
# read_model. Still MVP scope: `where`/`group_by`/`order_by`
|
|
370
505
|
# aren't drawn at all yet — real facts, not invented, just not
|
|
371
506
|
# this diagram's job yet.
|
|
507
|
+
#
|
|
508
|
+
# @param bluebook [Bluebook::Chapter] the chapter to project
|
|
509
|
+
# @return [String, nil] the rendered Mermaid flowchart source, or `nil` if the
|
|
510
|
+
# domain declares no `read_model` at all
|
|
372
511
|
def read_model_diagram(bluebook)
|
|
373
512
|
lines = bluebook.read_models.flat_map { |read_model| read_model_edges(read_model) }
|
|
374
513
|
return nil if lines.empty?
|
|
@@ -377,16 +516,22 @@ module Hecks
|
|
|
377
516
|
"#{header(bluebook.name, subject)}flowchart LR\n#{lines.uniq.join("\n")}\n"
|
|
378
517
|
end
|
|
379
518
|
|
|
519
|
+
# One edge per aggregate one read model is assembled from, into that
|
|
520
|
+
# read model's own subroutine-shaped node.
|
|
521
|
+
#
|
|
522
|
+
# @param read_model [Bluebook::ReadModel] the read model to draw
|
|
523
|
+
# @return [Array<String>] the rendered edge lines, one per entry in
|
|
524
|
+
# `read_model.to_h[:aggregate_heads]`
|
|
380
525
|
def read_model_edges(read_model)
|
|
381
526
|
shape = read_model.to_h
|
|
382
527
|
node = %(rm_#{shape[:name]}[["#{read_model_label(shape)}"]])
|
|
383
528
|
Array(shape[:aggregate_heads]).map do |head|
|
|
384
|
-
#
|
|
529
|
+
# **Quoted, not bare** — an edge label containing `[` or `]`
|
|
385
530
|
# (`accounts[]`, marking the "many" side) breaks Mermaid's own
|
|
386
531
|
# `|label|` parser outright if left unquoted: it reads the
|
|
387
|
-
# `[` as the
|
|
532
|
+
# `[` as the start of a new node shape mid-label, not text.
|
|
388
533
|
# Confirmed live against the real parser before this quoting
|
|
389
|
-
# existed — every
|
|
534
|
+
# existed — every other edge label in this file happens to be
|
|
390
535
|
# a bare word or already-quoted string, so this is the one
|
|
391
536
|
# spot that needed it.
|
|
392
537
|
label = head[:many] ? "#{head[:as]}[]" : head[:as]
|
|
@@ -394,6 +539,13 @@ module Hecks
|
|
|
394
539
|
end
|
|
395
540
|
end
|
|
396
541
|
|
|
542
|
+
# The read model's own subroutine node label — its name, with
|
|
543
|
+
# `(count)`/`(median: field)` appended for the two aggregations this
|
|
544
|
+
# diagram states.
|
|
545
|
+
#
|
|
546
|
+
# @param shape [Hash{Symbol => Object}] a `Bluebook::ReadModel#to_h` result;
|
|
547
|
+
# reads `:name`, `:count` and `:median_field`
|
|
548
|
+
# @return [String] the node's display label
|
|
397
549
|
def read_model_label(shape)
|
|
398
550
|
return "#{shape[:name]} (count)" if shape[:count]
|
|
399
551
|
return "#{shape[:name]} (median: #{shape[:median_field]})" if shape[:median_field]
|
|
@@ -403,50 +555,59 @@ module Hecks
|
|
|
403
555
|
|
|
404
556
|
# ── surface -> flowchart ─────────────────────────────────────────
|
|
405
557
|
|
|
406
|
-
#
|
|
558
|
+
# Renders one holder's own commands (and what each writes) and queries
|
|
559
|
+
# as a Mermaid flowchart.
|
|
560
|
+
#
|
|
561
|
+
# "What can I do to this, what can I ask about it" — one file per
|
|
407
562
|
# holder, unlike every other diagram here: `dispatch.mmd` already
|
|
408
563
|
# shows a command's own onward reaction chain, but never an
|
|
409
|
-
# aggregate's own
|
|
564
|
+
# aggregate's own full command/query menu in one place, and
|
|
410
565
|
# `roles.mmd` shows who issues a command without saying what else
|
|
411
566
|
# that same aggregate answers. This is the one diagram meant to
|
|
412
567
|
# be read starting from the aggregate, not from a verb or a fact.
|
|
413
568
|
#
|
|
414
|
-
# A
|
|
569
|
+
# A query is a diamond — a fifth shape, beside `dispatch.mmd`'s
|
|
415
570
|
# stadium/hexagon, `ports.mmd`'s trapezoid, and `read_models.mmd`'s
|
|
416
571
|
# subroutine: a question with an answer, not a verb that changes
|
|
417
572
|
# anything. Command edges are solid ("does"); query edges are
|
|
418
573
|
# dotted ("asks") — the same solid/dotted split `ports.mmd`
|
|
419
574
|
# already uses for "routes to:" versus "exposes".
|
|
420
575
|
#
|
|
421
|
-
# A
|
|
576
|
+
# A write target is a plain rectangle — a sixth shape, the first
|
|
422
577
|
# here with no special bracket at all: an attribute is the
|
|
423
578
|
# smallest, most passive thing this vocabulary names, a single
|
|
424
|
-
# field living
|
|
579
|
+
# field living inside the cylinder rather than a bounded thing of
|
|
425
580
|
# its own. `command.mutations` (`sets`/`increment`/`decrement`/
|
|
426
581
|
# `append`) was invisible everywhere before this — not just in a
|
|
427
|
-
# diagram, in
|
|
582
|
+
# diagram, in any projection, including the prose ones — despite
|
|
428
583
|
# being the single densest fact in the whole IR (53 real
|
|
429
584
|
# mutations across pizzas + banking). `dispatch.mmd` draws what a
|
|
430
|
-
# command
|
|
585
|
+
# command emits; this draws what it writes, the other half of
|
|
431
586
|
# "what actually happens" a command never showed before.
|
|
432
587
|
#
|
|
433
|
-
#
|
|
588
|
+
# The same attribute node merges across commands — real in
|
|
434
589
|
# banking: `Account.Credit` and `Account.Debit` both point at the
|
|
435
590
|
# same `balance` node, the same "one node, several incoming
|
|
436
591
|
# edges" merge `read_models.mmd` already does for an aggregate
|
|
437
592
|
# fed by several read_models.
|
|
438
593
|
#
|
|
439
|
-
#
|
|
594
|
+
# The label names the real source, not just the verb — an
|
|
440
595
|
# increment/decrement/set almost always takes its value from an
|
|
441
|
-
# argument, but not always the
|
|
596
|
+
# argument, but not always the same-named one: real in banking,
|
|
442
597
|
# `Account.Credit`'s own `balance` is incremented by its
|
|
443
598
|
# `amount` argument, and `LedgerEntry.Amend`'s own `amount` is
|
|
444
599
|
# incremented by its `adjustment` argument. A literal source
|
|
445
600
|
# (pizzas' own `Order.Purchase` sets `status` to the literal
|
|
446
601
|
# `"sold"`, not an argument at all) is named as verbatim as
|
|
447
602
|
# every other fact in this file. `append`'s own fields carry no
|
|
448
|
-
# single source at all — its own field
|
|
603
|
+
# single source at all — its own field names are the fact worth
|
|
449
604
|
# stating (real: `Order.AddTopping` appends `name, amount`).
|
|
605
|
+
#
|
|
606
|
+
# @param bluebook [Bluebook::Chapter] the chapter `holder` belongs to, for the
|
|
607
|
+
# file's re-run banner
|
|
608
|
+
# @param holder [Bluebook::Aggregate, Class] the aggregate or entity class (a
|
|
609
|
+
# `Bluebook::Entity` subclass) whose commands and queries to draw
|
|
610
|
+
# @return [String] the rendered Mermaid flowchart source
|
|
450
611
|
def surface_diagram(bluebook, holder)
|
|
451
612
|
lines = holder.commands.map do |command|
|
|
452
613
|
" #{holder.hecks_name}[(#{holder.hecks_name})] -->|does| #{command_node(holder.hecks_name, command.hecks_name)}"
|
|
@@ -464,10 +625,25 @@ module Hecks
|
|
|
464
625
|
"#{header(bluebook.name, subject)}flowchart LR\n#{lines.uniq.join("\n")}\n"
|
|
465
626
|
end
|
|
466
627
|
|
|
628
|
+
# Mermaid diamond-shaped node id and label for one query, qualified by
|
|
629
|
+
# its owning aggregate.
|
|
630
|
+
#
|
|
631
|
+
# @param aggregate_name [String] the query's owning aggregate/entity name
|
|
632
|
+
# @param query_name [String] the query's own name
|
|
633
|
+
# @return [String] the rendered `qry_<aggregate>_<query>{"..."}` node
|
|
467
634
|
def query_node(aggregate_name, query_name)
|
|
468
635
|
%(qry_#{aggregate_name}_#{query_name}{"#{aggregate_name}.#{query_name}"})
|
|
469
636
|
end
|
|
470
637
|
|
|
638
|
+
# One flowchart edge from a command to one attribute it mutates,
|
|
639
|
+
# labeled with what the mutation writes (see `mutation_label`).
|
|
640
|
+
#
|
|
641
|
+
# @param holder [Bluebook::Aggregate, Class] the aggregate or entity class (a
|
|
642
|
+
# `Bluebook::Entity` subclass) that declares `command` and the mutated attribute
|
|
643
|
+
# @param command [Class] the command class (a `Bluebook::Command` subclass) that
|
|
644
|
+
# declares `mutation`
|
|
645
|
+
# @param mutation [Bluebook::Mutation] the mutation to draw
|
|
646
|
+
# @return [String] the rendered `-->|"..."|` edge line
|
|
471
647
|
def mutation_edge(holder, command, mutation)
|
|
472
648
|
shape = mutation.to_h
|
|
473
649
|
label = mutation_label(shape)
|
|
@@ -475,11 +651,18 @@ module Hecks
|
|
|
475
651
|
%( #{command_node(holder.hecks_name, command.hecks_name)} -->|"#{label}"| #{target})
|
|
476
652
|
end
|
|
477
653
|
|
|
654
|
+
# The edge label naming what one mutation writes — the op, and either
|
|
655
|
+
# its bound field names (an append/delegate/corrects) or its value
|
|
656
|
+
# source (see `mutation_source_detail`).
|
|
657
|
+
#
|
|
658
|
+
# @param shape [Hash{Symbol => Object}] a `Bluebook::Mutation#to_h` result;
|
|
659
|
+
# reads `:op`, and either `:fields` or `:source`
|
|
660
|
+
# @return [String] the rendered `"<op>s: <detail>"` label text
|
|
478
661
|
def mutation_label(shape)
|
|
479
662
|
verb = "#{shape[:op]}s"
|
|
480
|
-
# `fields:` (not `source:`)
|
|
663
|
+
# `fields:` (not `source:`) is the multi-binding shape
|
|
481
664
|
# (`Mutation#to_h`'s own `[:append, :delegate, :corrects]`
|
|
482
|
-
# branch) — checked by the
|
|
665
|
+
# branch) — checked by the key's presence, not by re-listing
|
|
483
666
|
# which ops use it a second time here, the same lesson
|
|
484
667
|
# `Change.op`'s own `admits: Vocabulary::MutationOp` already
|
|
485
668
|
# drew (command.bluebook's own comment): a second list of "the
|
|
@@ -492,7 +675,7 @@ module Hecks
|
|
|
492
675
|
"#{verb}: #{detail}"
|
|
493
676
|
end
|
|
494
677
|
|
|
495
|
-
# A
|
|
678
|
+
# A literal value can contain a double quote of its own — real in
|
|
496
679
|
# banking: `Customer.Reinstate` sets `standing` to a rendered
|
|
497
680
|
# value-object literal, `{:value=>"good"}`, whose own embedded `"`
|
|
498
681
|
# broke this label's outer `|"..."|` quoting outright (caught by
|
|
@@ -501,33 +684,46 @@ module Hecks
|
|
|
501
684
|
# was caught). Swapped for a single quote here rather than
|
|
502
685
|
# escaped, the same "state it, don't invent it, just make it
|
|
503
686
|
# legal Mermaid" trade `read_models.mmd`'s own quoting fix made.
|
|
687
|
+
#
|
|
688
|
+
# @param source [Hash{Symbol => Object}] a classified mutation source: `{kind:
|
|
689
|
+
# "literal", value:}`, `{kind: "argument", name:}`, or `{kind: "state", name:}`
|
|
690
|
+
# @return [String] `source[:value]` quoted for a literal, `source[:name]` for an
|
|
691
|
+
# argument, or `source[:kind]` itself for any other kind
|
|
504
692
|
def mutation_source_detail(source)
|
|
505
693
|
case source[:kind]
|
|
506
694
|
when "literal" then "'#{source[:value].to_s.tr('"', "'")}'"
|
|
507
695
|
when "argument" then source[:name]
|
|
508
|
-
else
|
|
696
|
+
else
|
|
697
|
+
# A source kind this file has no real corpus example of yet — named, not hidden.
|
|
698
|
+
source[:kind]
|
|
509
699
|
end
|
|
510
700
|
end
|
|
511
701
|
|
|
702
|
+
# Mermaid plain-rectangle node id and label for one attribute a
|
|
703
|
+
# mutation writes, qualified by its owning holder.
|
|
704
|
+
#
|
|
705
|
+
# @param holder_name [String] the attribute's owning aggregate/entity name
|
|
706
|
+
# @param attribute_name [Symbol, String] the mutated attribute's own name
|
|
707
|
+
# @return [String] the rendered `attr_<holder>_<attribute>[<attribute>]` node
|
|
512
708
|
def attribute_node(holder_name, attribute_name)
|
|
513
709
|
%(attr_#{holder_name}_#{attribute_name}[#{attribute_name}])
|
|
514
710
|
end
|
|
515
711
|
|
|
516
712
|
# ── sagas -> stateDiagram-v2 ─────────────────────────────────────
|
|
517
713
|
|
|
518
|
-
# A
|
|
714
|
+
# A saga has a lifecycle too — the same `stateDiagram-v2` shape
|
|
519
715
|
# `lifecycle_diagram` already draws, one file per process_manager
|
|
520
716
|
# the same way lifecycle is one file per lifecycle-bearing holder.
|
|
521
717
|
# What's different is the label: a lifecycle's own edge is labeled
|
|
522
|
-
# by the
|
|
523
|
-
# something was
|
|
524
|
-
# that causes it (a saga advances because something
|
|
718
|
+
# by the command that causes it (an aggregate transitions because
|
|
719
|
+
# something was done to it); a saga's edge is labeled by the event
|
|
720
|
+
# that causes it (a saga advances because something happened,
|
|
525
721
|
# possibly nowhere near the saga itself) — the same command/event
|
|
526
722
|
# split `dispatch.mmd`'s own stadium/hexagon vocabulary already
|
|
527
723
|
# draws, here spent on which noun labels a stateDiagram-v2 edge
|
|
528
724
|
# instead.
|
|
529
725
|
#
|
|
530
|
-
#
|
|
726
|
+
# The label also names what the transition dispatches — a fact no
|
|
531
727
|
# existing diagram states for a saga at all: a lifecycle's own
|
|
532
728
|
# edge only ever names the one command that caused it; a saga's
|
|
533
729
|
# edge can fire several commands at once (real in banking:
|
|
@@ -538,12 +734,18 @@ module Hecks
|
|
|
538
734
|
# fires lands inside its own bluebook chapter, so this never needs
|
|
539
735
|
# `dispatch.mmd`'s own "triggers in X" cross-domain label.
|
|
540
736
|
#
|
|
541
|
-
#
|
|
737
|
+
# The compensating leg reads like any other — its own trigger is
|
|
542
738
|
# the literal string "refused" (`ProcessManager::REFUSED`, this
|
|
543
739
|
# language's own Trigger vocabulary), not invented text: a
|
|
544
740
|
# dispatch declined is exactly as real a cause of a state
|
|
545
741
|
# transition as an event announced, and the diagram states it
|
|
546
742
|
# exactly as verbatim as every other edge here does.
|
|
743
|
+
#
|
|
744
|
+
# @param bluebook [Bluebook::Chapter] the chapter `saga` belongs to, for the
|
|
745
|
+
# file's re-run banner
|
|
746
|
+
# @param saga [Bluebook::ProcessManager] the process manager whose states and
|
|
747
|
+
# dispatches to draw
|
|
748
|
+
# @return [String] the rendered Mermaid `stateDiagram-v2` source
|
|
547
749
|
def saga_diagram(bluebook, saga)
|
|
548
750
|
edges = saga.handlers.map { |handler| saga_edge(handler, saga) }
|
|
549
751
|
|
|
@@ -556,20 +758,29 @@ module Hecks
|
|
|
556
758
|
MERMAID
|
|
557
759
|
end
|
|
558
760
|
|
|
559
|
-
#
|
|
761
|
+
# One stateDiagram-v2 edge for one saga handler, labeled by the event
|
|
762
|
+
# that causes it and what it dispatches.
|
|
763
|
+
#
|
|
764
|
+
# The `REFUSED` edge's own dispatch list is partly derived now —
|
|
560
765
|
# per-dispatch saga compensation (`compensates`) moved a saga's own
|
|
561
|
-
# compensating dispatches
|
|
766
|
+
# compensating dispatches off the hand-written `on :refused` leg
|
|
562
767
|
# and onto whichever forward dispatch each one undoes, so
|
|
563
|
-
# `handler.dispatches` alone would render an
|
|
564
|
-
# edge for any saga using it — accurate to the
|
|
768
|
+
# `handler.dispatches` alone would render an empty compensating
|
|
769
|
+
# edge for any saga using it — accurate to the declaration, wrong
|
|
565
770
|
# about what the runtime actually does at refusal (it derives and
|
|
566
771
|
# fires every declared `compensates`, newest first). `saga` is
|
|
567
|
-
# passed through for exactly this — only the REFUSED handler needs
|
|
772
|
+
# passed through for exactly this — only the `REFUSED` handler needs
|
|
568
773
|
# it, every other edge's own `handler.dispatches` already says
|
|
569
774
|
# everything real about it.
|
|
775
|
+
#
|
|
776
|
+
# @param handler [Bluebook::ProcessManagerHandler] the handler row to draw as
|
|
777
|
+
# an edge
|
|
778
|
+
# @param saga [Bluebook::ProcessManager] the process manager `handler` belongs
|
|
779
|
+
# to, needed only to derive the `REFUSED` edge's compensating dispatches
|
|
780
|
+
# @return [String] the rendered `-->` stateDiagram-v2 edge line
|
|
570
781
|
def saga_edge(handler, saga)
|
|
571
782
|
label = handler.event_type
|
|
572
|
-
#
|
|
783
|
+
# Derived first, then the hand-written body — the same order
|
|
573
784
|
# `SagaInterpreter#unwind` actually runs them in (every
|
|
574
785
|
# completed leg's own `compensates` before this leg's own
|
|
575
786
|
# hand-written dispatches), not declaration order on the page.
|
|
@@ -585,33 +796,41 @@ module Hecks
|
|
|
585
796
|
# derives and fires (newest-first, at actual refusal time; this
|
|
586
797
|
# diagram states them in declaration order, since it draws the
|
|
587
798
|
# saga's own shape, not one instance's own runtime history).
|
|
799
|
+
#
|
|
800
|
+
# @param saga [Bluebook::ProcessManager] the process manager whose declared
|
|
801
|
+
# compensations to list
|
|
802
|
+
# @return [Array<String>] the name of each compensating command declared anywhere
|
|
803
|
+
# in `saga`, in declaration order
|
|
588
804
|
def derived_compensations(saga)
|
|
589
805
|
saga.handlers.flat_map { |handler| handler.dispatches.filter_map { |dispatch| dispatch.compensates&.command_name } }
|
|
590
806
|
end
|
|
591
807
|
|
|
592
808
|
# ── frameworks -> flowchart ─────────────────────────────────────
|
|
593
809
|
|
|
594
|
-
#
|
|
595
|
-
#
|
|
810
|
+
# Renders the whole domain's declared `uses_framework` attachments and
|
|
811
|
+
# cross-domain policy targets as a Mermaid flowchart.
|
|
812
|
+
#
|
|
813
|
+
# Every other diagram in this file stays inside one domain's own
|
|
814
|
+
# boundary — this is the one that steps outside it. A real domain
|
|
596
815
|
# depends on another domain's own aggregates in exactly two ways:
|
|
597
816
|
# `uses_framework "X"` in its `.hecksagon` (`Hecksagon#framework_
|
|
598
|
-
# members`), which loads X's whole bluebook into
|
|
817
|
+
# members`), which loads X's whole bluebook into this registry,
|
|
599
818
|
# unconditionally, the moment this domain boots; or a policy's own
|
|
600
819
|
# `across "X"` (`Policy#target_domain`), which only reaches X when
|
|
601
820
|
# the policy's declared event actually fires. Same underlying
|
|
602
821
|
# fact `dispatch.mmd`'s own `trigger_edge` already draws from the
|
|
603
822
|
# command's side ("triggers in X") — this draws it again from the
|
|
604
|
-
#
|
|
823
|
+
# domain's side, next to the structural `uses_framework` fact
|
|
605
824
|
# `dispatch.mmd` never sees at all (that lives in the `.hecksagon`,
|
|
606
825
|
# which no other diagram here is handed).
|
|
607
826
|
#
|
|
608
|
-
#
|
|
609
|
-
# — a whole domain is drawn as
|
|
827
|
+
# Neither this domain nor each dependency gets the holders() treatment
|
|
828
|
+
# — a whole domain is drawn as one cylinder, the same "a bounded,
|
|
610
829
|
# addressable thing" shape every other diagram here already spends
|
|
611
830
|
# on a single aggregate, just scaled up one level: a domain is a
|
|
612
831
|
# bigger box the same kind of box lives inside.
|
|
613
832
|
#
|
|
614
|
-
#
|
|
833
|
+
# Dotted for `attaches`, solid for `reaches across` — the reverse
|
|
615
834
|
# of which fact is "always true" between the two: attaching a
|
|
616
835
|
# framework is a standing declaration, true every time this domain
|
|
617
836
|
# boots, so it gets the same dotted "this always belongs" treatment
|
|
@@ -621,14 +840,21 @@ module Hecks
|
|
|
621
840
|
# draws for the identical fact, kept solid here so the same
|
|
622
841
|
# relationship reads the same way in both diagrams.
|
|
623
842
|
#
|
|
624
|
-
# `options[:hecksagon]`
|
|
625
|
-
#
|
|
843
|
+
# `options[:hecksagon]` is the one diagram in this file that needs
|
|
844
|
+
# more than `bluebook` — `framework_members` lives on the
|
|
626
845
|
# `Hecksagon`, a sibling IR object `bin/project_diagrams` already
|
|
627
846
|
# has in hand (`registry.hecksagon(chapter_name)`) but `bluebook`
|
|
628
847
|
# itself carries no reference to. No hecksagon handed in (an older
|
|
629
848
|
# caller, or a spec that doesn't care) just means no frameworks.mmd
|
|
630
849
|
# — same "nothing to state" skip every other diagram here already
|
|
631
850
|
# takes when its own underlying data is empty.
|
|
851
|
+
#
|
|
852
|
+
# @param bluebook [Bluebook::Chapter] the chapter to project
|
|
853
|
+
# @param hecksagon [Bluebook::Hecksagon, nil] the chapter's `.hecksagon`, holding
|
|
854
|
+
# its `framework_members`, or `nil` if the caller has none to hand in
|
|
855
|
+
# @return [String, nil] the rendered Mermaid flowchart source, or `nil` if
|
|
856
|
+
# `hecksagon` is `nil`, or the domain declares no `uses_framework` and no
|
|
857
|
+
# cross-domain policy target
|
|
632
858
|
def frameworks_diagram(bluebook, hecksagon)
|
|
633
859
|
return nil unless hecksagon
|
|
634
860
|
|
|
@@ -641,6 +867,16 @@ module Hecks
|
|
|
641
867
|
"#{header(bluebook.name, subject)}flowchart LR\n#{lines.uniq.join("\n")}\n"
|
|
642
868
|
end
|
|
643
869
|
|
|
870
|
+
# One flowchart edge from this domain to a dependency, dotted for a
|
|
871
|
+
# standing `attaches` and solid for a `reaches across` that only fires
|
|
872
|
+
# when a policy does.
|
|
873
|
+
#
|
|
874
|
+
# @param from [String] this domain's own name
|
|
875
|
+
# @param label [String] the edge label, `"attaches"` or `"reaches across"`
|
|
876
|
+
# @param to [String, Symbol] the dependency domain's name
|
|
877
|
+
# @param dotted [Boolean] whether to render a dotted (`true`) or solid (`false`)
|
|
878
|
+
# arrow
|
|
879
|
+
# @return [String] the rendered edge line
|
|
644
880
|
def domain_edge(from, label, to, dotted:)
|
|
645
881
|
arrow = dotted ? "-.->" : "-->"
|
|
646
882
|
%( #{from}[(#{from})] #{arrow}|#{label}| #{to}[(#{to})])
|