hecks 1.4.0 → 1.5.1
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 +65 -0
- data/lib/hecks/adapters/driven/folder.rb +73 -0
- data/lib/hecks/adapters/driven/google_authentication.rb +25 -4
- data/lib/hecks/adapters/driven/governance_authorization.rb +57 -6
- data/lib/hecks/adapters/driven/heki/journal.rb +8 -0
- data/lib/hecks/adapters/driven/heki/saga_store.rb +53 -7
- data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
- data/lib/hecks/adapters/driven/heki.rb +93 -2
- data/lib/hecks/adapters/driven/identity_registry.rb +10 -0
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +22 -0
- 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 +35 -7
- data/lib/hecks/adapters/driven/lambda.rb +43 -2
- data/lib/hecks/adapters/driven/local_storage.rb +67 -1
- data/lib/hecks/adapters/driven/memory.rb +13 -13
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +20 -0
- data/lib/hecks/adapters/driven/prism.rb +46 -7
- data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
- 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 +2 -0
- data/lib/hecks/adapters/driving/github_webhook.rb +14 -1
- data/lib/hecks/behaviors/dsl.rb +58 -0
- data/lib/hecks/behaviors/expectations.rb +168 -7
- data/lib/hecks/behaviors/ir.rb +11 -0
- data/lib/hecks/behaviors/rspec.rb +8 -0
- data/lib/hecks/behaviors/runner.rb +19 -0
- data/lib/hecks/behaviors.rb +8 -0
- data/lib/hecks/bluebook/aggregate.rb +30 -0
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +7 -0
- data/lib/hecks/bluebook/assembly/build.rb +25 -4
- data/lib/hecks/bluebook/assembly/contract.rb +66 -14
- data/lib/hecks/bluebook/assembly/contracts.rb +24 -19
- data/lib/hecks/bluebook/assembly/marks.rb +143 -14
- data/lib/hecks/bluebook/assembly/specializer.rb +19 -0
- data/lib/hecks/bluebook/assembly.rb +25 -7
- data/lib/hecks/bluebook/attribute.rb +17 -3
- data/lib/hecks/bluebook/behaviour/aggregate.rb +32 -0
- data/lib/hecks/bluebook/behaviour/attribute.rb +13 -0
- data/lib/hecks/bluebook/behaviour/chapter.rb +55 -2
- data/lib/hecks/bluebook/behaviour/command.rb +33 -3
- data/lib/hecks/bluebook/behaviour/domain_port.rb +20 -0
- data/lib/hecks/bluebook/behaviour/entity.rb +12 -0
- data/lib/hecks/bluebook/behaviour/hexagon.rb +26 -0
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +23 -2
- data/lib/hecks/bluebook/behaviour/policy.rb +34 -9
- data/lib/hecks/bluebook/behaviour/process_manager.rb +32 -1
- data/lib/hecks/bluebook/behaviour/query.rb +5 -0
- data/lib/hecks/bluebook/behaviour/read_model.rb +21 -0
- data/lib/hecks/bluebook/behaviour/traits.rb +36 -0
- data/lib/hecks/bluebook/behaviour/value_object.rb +17 -5
- data/lib/hecks/bluebook/chapter.rb +23 -0
- data/lib/hecks/bluebook/command.rb +53 -8
- data/lib/hecks/bluebook/domain_port.rb +25 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +196 -37
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +79 -42
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +59 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +385 -53
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +135 -21
- data/lib/hecks/bluebook/dsl/command_builder.rb +189 -27
- data/lib/hecks/bluebook/dsl/entity_builder.rb +139 -9
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +41 -0
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +23 -6
- data/lib/hecks/bluebook/dsl/policy_builder.rb +60 -19
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +79 -15
- data/lib/hecks/bluebook/dsl/query_builder.rb +33 -4
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +102 -27
- data/lib/hecks/bluebook/dsl/rule_reference.rb +60 -8
- data/lib/hecks/bluebook/dsl/translation_builder.rb +146 -40
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +55 -7
- data/lib/hecks/bluebook/entity.rb +29 -0
- data/lib/hecks/bluebook/expression/ast_json.rb +115 -23
- data/lib/hecks/bluebook/expression/ast_reader.rb +29 -0
- data/lib/hecks/bluebook/expression/canonical_form.rb +48 -9
- data/lib/hecks/bluebook/expression/evaluator.rb +207 -29
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +36 -0
- data/lib/hecks/bluebook/expression/resolver.rb +320 -77
- data/lib/hecks/bluebook/hexagon.rb +34 -0
- data/lib/hecks/bluebook/lifecycle.rb +11 -0
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -0
- data/lib/hecks/bluebook/meta_validator/judge.rb +30 -26
- data/lib/hecks/bluebook/meta_validator/plan.rb +43 -8
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +1 -0
- data/lib/hecks/bluebook/meta_validator/readings.rb +157 -7
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +24 -4
- data/lib/hecks/bluebook/meta_validator/shapes.rb +141 -0
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +149 -23
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +5 -4
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +1 -0
- data/lib/hecks/bluebook/meta_validator.rb +180 -84
- data/lib/hecks/bluebook/model_check.rb +268 -24
- data/lib/hecks/bluebook/pattern_subset.rb +23 -1
- data/lib/hecks/bluebook/process_manager.rb +13 -0
- 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 +44 -0
- data/lib/hecks/bluebook/query.rb +27 -0
- data/lib/hecks/bluebook/read_model.rb +21 -1
- data/lib/hecks/bluebook/reference.rb +21 -8
- data/lib/hecks/bluebook/smoke_test.rb +29 -6
- data/lib/hecks/bluebook/synthesizer.rb +34 -0
- data/lib/hecks/bluebook/translation.rb +30 -1
- data/lib/hecks/bluebook/value_object.rb +23 -5
- data/lib/hecks/bluebook.rb +3 -4
- data/lib/hecks/codemod.rb +107 -20
- data/lib/hecks/construct.rb +15 -1
- data/lib/hecks/corpus.rb +146 -25
- data/lib/hecks/deploy/bluebook/deploy.bluebook +105 -0
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
- data/lib/hecks/deploy/oidc.json +5 -0
- data/lib/hecks/doc/reference.rb +185 -16
- data/lib/hecks/embryonaut_bluebook.rb +32 -9
- data/lib/hecks/facade/handle.rb +76 -3
- data/lib/hecks/facade/surface/aggregate_door.rb +8 -0
- data/lib/hecks/forms/field_shape.rb +3 -0
- data/lib/hecks/forms/page.rb +14 -0
- data/lib/hecks/forms/port_argument.rb +12 -0
- data/lib/hecks/forms/query_form_renderer.rb +63 -0
- data/lib/hecks/forms/record_renderer.rb +58 -0
- data/lib/hecks/forms/record_table.rb +27 -0
- data/lib/hecks/forms/reference_options.rb +24 -0
- data/lib/hecks/forms/value_object_shape.rb +10 -0
- data/lib/hecks/fqn.rb +58 -0
- data/lib/hecks/framework/bluebook/compliance.bluebook +221 -0
- data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
- data/lib/hecks/framework/oidc.json +15 -0
- data/lib/hecks/framework.rb +43 -20
- data/lib/hecks/freezer.rb +17 -1
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +159 -24
- data/lib/hecks/fuzzing/combination_miner.rb +59 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +109 -8
- data/lib/hecks/fuzzing/coverage_campaign.rb +56 -13
- data/lib/hecks/fuzzing/differential.rb +34 -0
- data/lib/hecks/fuzzing/domain_generator.rb +188 -11
- data/lib/hecks/fuzzing/era_boundary.rb +45 -15
- data/lib/hecks/fuzzing/form_census.rb +86 -0
- data/lib/hecks/fuzzing/generated_domain_check.rb +76 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +39 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +79 -22
- data/lib/hecks/fuzzing/nondeterministic.rb +13 -1
- data/lib/hecks/fuzzing/persistence_parity.rb +95 -3
- data/lib/hecks/fuzzing/properties/corrections.rb +25 -0
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +158 -14
- data/lib/hecks/fuzzing/properties/guards.rb +44 -0
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +48 -0
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +18 -0
- data/lib/hecks/fuzzing/properties/outbox.rb +49 -11
- data/lib/hecks/fuzzing/properties/querying.rb +68 -14
- data/lib/hecks/fuzzing/properties.rb +24 -15
- data/lib/hecks/fuzzing/qa_settings.rb +12 -0
- data/lib/hecks/fuzzing/replay.rb +137 -29
- data/lib/hecks/fuzzing/rotation_priority.rb +41 -21
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +46 -20
- data/lib/hecks/fuzzing/self_consistency.rb +189 -40
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +12 -6
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +1 -1
- data/lib/hecks/fuzzing/sequence_generator.rb +47 -0
- data/lib/hecks/fuzzing/shrinker.rb +114 -2
- data/lib/hecks/fuzzing/structural_skips.rb +18 -4
- data/lib/hecks/fuzzing/sweep_depth.rb +8 -0
- data/lib/hecks/fuzzing/target_capabilities.rb +61 -14
- data/lib/hecks/fuzzing/value_generator.rb +98 -10
- data/lib/hecks/grammar/evolve.rb +178 -2
- data/lib/hecks/grammar.rb +46 -0
- data/lib/hecks/ir.rb +38 -7
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
- data/lib/hecks/literal.rb +32 -0
- data/lib/hecks/naming.rb +88 -7
- data/lib/hecks/ports/access_control.rb +5 -10
- data/lib/hecks/ports/authorization.rb +3 -6
- data/lib/hecks/ports/identity_assignment.rb +1 -2
- data/lib/hecks/ports/identity_resolution.rb +1 -2
- data/lib/hecks/ports/key_vault.port +6 -0
- data/lib/hecks/ports/key_vault.rb +85 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +6 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +38 -2
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +40 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +92 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +8 -0
- data/lib/hecks/ports/query/in_memory.rb +39 -6
- data/lib/hecks/ports/query/ordering.rb +15 -0
- data/lib/hecks/ports.rb +1 -0
- data/lib/hecks/projections/bootstrap_table.rb +43 -8
- data/lib/hecks/projections/deploy/fargate.rb +666 -0
- data/lib/hecks/projections/deploy/lambda.rb +2423 -0
- data/lib/hecks/projections/deploy/shared.rb +624 -0
- data/lib/hecks/projections/diagrams.rb +243 -7
- data/lib/hecks/projections/glossary/html.rb +88 -0
- data/lib/hecks/projections/glossary/markdown.rb +47 -0
- data/lib/hecks/projections/glossary/mermaid.rb +48 -0
- data/lib/hecks/projections/glossary/sentences.rb +105 -0
- data/lib/hecks/projections/glossary.rb +161 -19
- data/lib/hecks/projections/model/deviations.rb +44 -0
- data/lib/hecks/projections/model.rb +51 -1
- data/lib/hecks/projections/oidc.rb +40 -3
- data/lib/hecks/projections/parser_table.rb +49 -8
- data/lib/hecks/projections/reference.rb +12 -0
- data/lib/hecks/projections/rust_vocabulary.rb +219 -16
- data/lib/hecks/projections/shape.rb +8 -0
- data/lib/hecks/projections/statements.rb +63 -16
- data/lib/hecks/projections/vocabulary.rb +17 -0
- data/lib/hecks/projections.rb +3 -0
- data/lib/hecks/projector/cli_projector.rb +218 -10
- data/lib/hecks/projector/docs_projector.rb +145 -19
- data/lib/hecks/projector/exporter.rb +65 -11
- data/lib/hecks/projector/ir_projector.rb +6 -0
- data/lib/hecks/projector/narrate_projector.rb +136 -15
- data/lib/hecks/projector/target.rb +47 -10
- data/lib/hecks/projector.rb +101 -19
- data/lib/hecks/query_ir.rb +47 -0
- data/lib/hecks/query_specification/common/null_policy.rb +5 -3
- data/lib/hecks/rendering.rb +6 -0
- data/lib/hecks/router/namespace_installer.rb +13 -0
- data/lib/hecks/router.rb +55 -0
- data/lib/hecks/runtime/aggregate_lock.rb +9 -0
- data/lib/hecks/runtime/boot_gates.rb +18 -0
- data/lib/hecks/runtime/caller.rb +32 -0
- data/lib/hecks/runtime/capability_graph.rb +11 -0
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +23 -21
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +14 -15
- data/lib/hecks/runtime/command_interpreter.rb +42 -17
- data/lib/hecks/runtime/command_rules/admissibility.rb +165 -14
- data/lib/hecks/runtime/command_rules/arithmetic.rb +17 -5
- data/lib/hecks/runtime/command_rules/references.rb +118 -28
- data/lib/hecks/runtime/dependency_planning.rb +45 -0
- data/lib/hecks/runtime/dispatcher.rb +28 -50
- data/lib/hecks/runtime/entity_element.rb +161 -8
- data/lib/hecks/runtime/entity_interpreter.rb +44 -9
- data/lib/hecks/runtime/errors.rb +18 -4
- data/lib/hecks/runtime/event.rb +10 -5
- data/lib/hecks/runtime/identity.rb +71 -3
- data/lib/hecks/runtime/instance.rb +67 -7
- data/lib/hecks/runtime/interpreting.rb +13 -5
- data/lib/hecks/runtime/invocation.rb +118 -36
- data/lib/hecks/runtime/loader.rb +94 -8
- data/lib/hecks/runtime/outbox.rb +145 -7
- data/lib/hecks/runtime/policy_interpreter.rb +22 -9
- data/lib/hecks/runtime/port_operation_interpreter.rb +20 -0
- data/lib/hecks/runtime/query_interpreter.rb +40 -12
- data/lib/hecks/runtime/reaction_invocation.rb +53 -8
- data/lib/hecks/runtime/read_model_interpreter.rb +23 -7
- data/lib/hecks/runtime/rebuild_sweep.rb +28 -0
- data/lib/hecks/runtime/reference_hop.rb +42 -0
- data/lib/hecks/runtime/refusal_wording.rb +50 -0
- data/lib/hecks/runtime/registry/saga_persistence.rb +11 -0
- data/lib/hecks/runtime/registry/verification.rb +119 -4
- data/lib/hecks/runtime/registry.rb +157 -4
- data/lib/hecks/runtime/remote_dispatcher.rb +92 -6
- data/lib/hecks/runtime/routing.rb +27 -2
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +10 -12
- data/lib/hecks/runtime/saga_interpreter.rb +27 -13
- data/lib/hecks/runtime/tenant_check.rb +26 -6
- data/lib/hecks/runtime/tenant_scope.rb +18 -0
- data/lib/hecks/runtime/value/coercion.rb +255 -33
- data/lib/hecks/runtime/value/entity_list_coercion.rb +102 -30
- data/lib/hecks/runtime/value.rb +50 -0
- data/lib/hecks/runtime.rb +32 -0
- data/lib/hecks/storehouse.rb +305 -9
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
- data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks.rb +79 -1
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -0
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +12 -2
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +29 -5
- metadata +14 -5
- data/lib/hecks/codemod/legacy_dispatch_args.rb +0 -299
- data/lib/hecks/codemod/legacy_dispatch_recorder.rb +0 -186
- data/lib/hecks/deprecation.rb +0 -95
- data/lib/hecks/framework/bluebook/compliance.bluebook +0 -1
|
@@ -5,16 +5,18 @@ module Hecks
|
|
|
5
5
|
module Projector
|
|
6
6
|
# A bluebook, projected as prose an SME can read back and confirm.
|
|
7
7
|
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
8
|
+
# ## What this is for
|
|
9
|
+
#
|
|
10
|
+
# `DocsProjector` already answers "what can I call and what does it
|
|
11
|
+
# want" for the person implementing against a domain — tables of
|
|
12
|
+
# arguments, shapes, refusal reasons. That is the wrong register for
|
|
13
|
+
# the person who can actually say whether the domain is right: the
|
|
14
|
+
# subject-matter expert who knows what an account is and has never
|
|
15
|
+
# read a markdown table in their life. This projects the same IR as
|
|
16
|
+
# sentences instead — "Debit — take money out. Issued by a Teller.
|
|
17
|
+
# It only goes through if the balance covers it." — so a domain can
|
|
18
|
+
# be read back to the person who can validate it without them
|
|
19
|
+
# learning the DSL first.
|
|
18
20
|
#
|
|
19
21
|
# Same source, same guarantee `DocsProjector` gives: nothing here is
|
|
20
22
|
# invented. Every sentence quotes a `description`, `goal`, or `given`
|
|
@@ -24,17 +26,30 @@ module Hecks
|
|
|
24
26
|
# (`Projector.call(:narrate, bluebook: ...)`), same aggregate-scoping
|
|
25
27
|
# via `options[:aggregate]`.
|
|
26
28
|
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
29
|
+
# ## What it does not do
|
|
30
|
+
#
|
|
31
|
+
# Replace `DocsProjector`. A shape table still says "id of a
|
|
32
|
+
# Customer" more precisely than any sentence would, and an
|
|
33
|
+
# implementer still wants that. This is the other document the same
|
|
34
|
+
# IR is owed — one written for the reader who is being asked "is
|
|
35
|
+
# this right?", not "how do I call it?"
|
|
32
36
|
module NarrateProjector
|
|
33
37
|
module_function
|
|
34
38
|
|
|
39
|
+
# Projects `bluebook` as prose an SME can read back and confirm.
|
|
40
|
+
#
|
|
35
41
|
# `options[:heading]` sets the top heading level, exactly as
|
|
36
42
|
# `DocsProjector` does — so this, too, can be spliced into a larger
|
|
37
43
|
# document rather than always starting at H1.
|
|
44
|
+
#
|
|
45
|
+
# @param bluebook [Bluebook::Behaviour::Chapter] the chapter to narrate
|
|
46
|
+
# @param options [Hash] optional inputs
|
|
47
|
+
# @option options [Integer, String] :heading the top heading level; defaults to 1
|
|
48
|
+
# @option options [String, Symbol, nil] :aggregate narrows the narrative to one
|
|
49
|
+
# aggregate, omitting the chapter intro and reactions sections
|
|
50
|
+
# @return [String] the narrative, as Markdown prose, ending in a newline
|
|
51
|
+
# @raise [Runtime::NotFound] if `options[:aggregate]` names no aggregate `bluebook`
|
|
52
|
+
# declares
|
|
38
53
|
def call(bluebook:, options: {})
|
|
39
54
|
depth = (options[:heading] || 1).to_i
|
|
40
55
|
only = options[:aggregate]
|
|
@@ -50,6 +65,12 @@ module Hecks
|
|
|
50
65
|
|
|
51
66
|
# ── the chapter ───────────────────────────────────────────────────
|
|
52
67
|
|
|
68
|
+
# Narrates the chapter-level intro: its vision, classification, former
|
|
69
|
+
# name, and the aggregates it's told through.
|
|
70
|
+
#
|
|
71
|
+
# @param bluebook [Bluebook::Behaviour::Chapter] the chapter to narrate
|
|
72
|
+
# @param depth [Integer] the heading level for the chapter's own title
|
|
73
|
+
# @return [String] the intro's Markdown prose
|
|
53
74
|
def chapter_intro(bluebook, depth)
|
|
54
75
|
parts = [DocsProjector.h(depth, bluebook.name)]
|
|
55
76
|
parts << bluebook.vision if bluebook.vision
|
|
@@ -66,6 +87,12 @@ module Hecks
|
|
|
66
87
|
|
|
67
88
|
# ── one aggregate ─────────────────────────────────────────────────
|
|
68
89
|
|
|
90
|
+
# Narrates one aggregate: its description, identity, references,
|
|
91
|
+
# lifecycle, commands, queries, and nested entities.
|
|
92
|
+
#
|
|
93
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate to narrate
|
|
94
|
+
# @param depth [Integer] the heading level for the aggregate's own title
|
|
95
|
+
# @return [String] the aggregate's Markdown prose
|
|
69
96
|
def aggregate_narrative(aggregate, depth)
|
|
70
97
|
parts = [DocsProjector.h(depth, aggregate.hecks_name)]
|
|
71
98
|
parts << aggregate.description if aggregate.description
|
|
@@ -86,12 +113,23 @@ module Hecks
|
|
|
86
113
|
parts.compact.join("\n\n")
|
|
87
114
|
end
|
|
88
115
|
|
|
116
|
+
# Names how a holder is identified.
|
|
117
|
+
#
|
|
118
|
+
# @param holder [Bluebook::Aggregate, Bluebook::Entity] the holder to describe
|
|
119
|
+
# @return [String, nil] a sentence naming `holder`'s identity fields, or nil if
|
|
120
|
+
# it declares none
|
|
89
121
|
def identity_sentence(holder)
|
|
90
122
|
return nil if holder.identity_heads.empty?
|
|
91
123
|
|
|
92
124
|
"Every #{holder.hecks_name} is identified by its #{to_sentence_list(holder.identity_heads.map { |h| "`#{h}`" })}."
|
|
93
125
|
end
|
|
94
126
|
|
|
127
|
+
# Narrates one entity nested under `aggregate`.
|
|
128
|
+
#
|
|
129
|
+
# @param aggregate [Bluebook::Aggregate] the entity's own owning aggregate
|
|
130
|
+
# @param entity [Bluebook::Entity] the entity to narrate
|
|
131
|
+
# @param depth [Integer] the heading level for the entity's own title
|
|
132
|
+
# @return [String] the entity's Markdown prose
|
|
95
133
|
def entity_narrative(aggregate, entity, depth)
|
|
96
134
|
parts = [DocsProjector.h(depth, "#{entity.hecks_name} (within #{aggregate.hecks_name})")]
|
|
97
135
|
parts << entity.description if entity.description
|
|
@@ -106,6 +144,11 @@ module Hecks
|
|
|
106
144
|
|
|
107
145
|
# ── the machine ───────────────────────────────────────────────────
|
|
108
146
|
|
|
147
|
+
# Narrates a holder's own lifecycle transitions.
|
|
148
|
+
#
|
|
149
|
+
# @param holder [Bluebook::Aggregate, Bluebook::Entity] the holder to describe
|
|
150
|
+
# @return [String, nil] a sentence per lifecycle transition, or nil if `holder`
|
|
151
|
+
# declares no lifecycle
|
|
109
152
|
def lifecycle_narrative(holder)
|
|
110
153
|
lifecycle = holder.lifecycle or return nil
|
|
111
154
|
|
|
@@ -120,6 +163,13 @@ module Hecks
|
|
|
120
163
|
|
|
121
164
|
# ── the verbs ─────────────────────────────────────────────────────
|
|
122
165
|
|
|
166
|
+
# Narrates every command a holder declares.
|
|
167
|
+
#
|
|
168
|
+
# @param holder [Bluebook::Aggregate, Bluebook::Entity] the holder whose
|
|
169
|
+
# commands to narrate
|
|
170
|
+
# @param depth [Integer] the heading level for the "What can happen" section
|
|
171
|
+
# @return [String, nil] the section's Markdown prose, or nil if `holder`
|
|
172
|
+
# declares no command
|
|
123
173
|
def verbs_narrative(holder, depth)
|
|
124
174
|
return nil if holder.commands.empty?
|
|
125
175
|
|
|
@@ -136,6 +186,10 @@ module Hecks
|
|
|
136
186
|
# sentence depends on anything before it. Same nil-or-string +
|
|
137
187
|
# `compact.join` shape `aggregate_narrative`/`entity_narrative`
|
|
138
188
|
# already use above for the identical reason.
|
|
189
|
+
#
|
|
190
|
+
# @param command [Bluebook::Command] the command to narrate
|
|
191
|
+
# @param holder [Bluebook::Aggregate, Bluebook::Entity] `command`'s own holder
|
|
192
|
+
# @return [String] the command's own paragraph
|
|
139
193
|
def command_paragraph(command, holder)
|
|
140
194
|
[
|
|
141
195
|
command_headline_sentence(command),
|
|
@@ -153,10 +207,18 @@ module Hecks
|
|
|
153
207
|
# exactly as declared, not recased to fit mid-sentence, because the
|
|
154
208
|
# promise this whole projector makes is that a sentence here is a
|
|
155
209
|
# sentence the chapter actually wrote.
|
|
210
|
+
#
|
|
211
|
+
# @param command [Bluebook::Command] the command to name
|
|
212
|
+
# @return [String] the command's own bold name, plus its goal if it declares one
|
|
156
213
|
def command_headline_sentence(command)
|
|
157
214
|
"**#{command.hecks_name}**#{command.goal ? " — #{command.goal}." : '.'}"
|
|
158
215
|
end
|
|
159
216
|
|
|
217
|
+
# Names who issues a command.
|
|
218
|
+
#
|
|
219
|
+
# @param command [Bluebook::Command] the command to describe
|
|
220
|
+
# @return [String, nil] a sentence naming who issues `command`, or nil if it
|
|
221
|
+
# declares no role
|
|
160
222
|
def command_role_sentence(command)
|
|
161
223
|
return nil unless command.role
|
|
162
224
|
|
|
@@ -168,12 +230,21 @@ module Hecks
|
|
|
168
230
|
# `Command#acts_on`'s own comment), so reading it directly here would
|
|
169
231
|
# tell an SME that `LedgerEntry.Amend` brings a new ledger entry into
|
|
170
232
|
# being, which is exactly backwards.
|
|
233
|
+
# @param command [Bluebook::Command] the command to check
|
|
234
|
+
# @param holder [Bluebook::Aggregate, Bluebook::Entity] `command`'s own holder
|
|
235
|
+
# @return [String, nil] a sentence saying `command` creates `holder`, or nil if
|
|
236
|
+
# `command` acts on an existing record instead
|
|
171
237
|
def command_creation_sentence(command, holder)
|
|
172
238
|
return nil unless command.acts_on.nil?
|
|
173
239
|
|
|
174
240
|
"This is how a new #{holder.hecks_name} comes into being."
|
|
175
241
|
end
|
|
176
242
|
|
|
243
|
+
# Lists a command's own non-reference arguments.
|
|
244
|
+
#
|
|
245
|
+
# @param command [Bluebook::Command] the command to describe
|
|
246
|
+
# @return [String, nil] a sentence listing `command`'s non-reference arguments,
|
|
247
|
+
# or nil if it declares none
|
|
177
248
|
def command_arguments_sentence(command)
|
|
178
249
|
arguments = command.attributes.reject(&:reference?)
|
|
179
250
|
return nil if arguments.empty?
|
|
@@ -181,6 +252,11 @@ module Hecks
|
|
|
181
252
|
"It takes #{to_sentence_list(arguments.map { |a| Forms::Humanize.label(a.name.to_s).downcase })}."
|
|
182
253
|
end
|
|
183
254
|
|
|
255
|
+
# Names what a command references.
|
|
256
|
+
#
|
|
257
|
+
# @param command [Bluebook::Command] the command to describe
|
|
258
|
+
# @return [String, nil] a sentence naming what `command` references, or nil if
|
|
259
|
+
# it declares no reference argument
|
|
184
260
|
def command_references_sentence(command)
|
|
185
261
|
refs = command.attributes.select(&:reference?)
|
|
186
262
|
return nil if refs.empty?
|
|
@@ -188,6 +264,12 @@ module Hecks
|
|
|
188
264
|
"It's aimed at one existing #{to_sentence_list(refs.map { |r| r.type.target_name })}, by id."
|
|
189
265
|
end
|
|
190
266
|
|
|
267
|
+
# Lists a command's own required conditions.
|
|
268
|
+
#
|
|
269
|
+
# @param command [Bluebook::Command] the command to describe
|
|
270
|
+
# @param holder [Bluebook::Aggregate, Bluebook::Entity] `command`'s own holder
|
|
271
|
+
# @return [String, nil] a sentence listing every required condition, or nil if
|
|
272
|
+
# `command` declares none
|
|
191
273
|
def command_conditions_sentence(command, holder)
|
|
192
274
|
conditions = conditions_of(command, holder)
|
|
193
275
|
return nil if conditions.empty?
|
|
@@ -195,6 +277,11 @@ module Hecks
|
|
|
195
277
|
"It only goes through if #{conditions.join('; ')}."
|
|
196
278
|
end
|
|
197
279
|
|
|
280
|
+
# Lists what a command guarantees when it succeeds.
|
|
281
|
+
#
|
|
282
|
+
# @param command [Bluebook::Command] the command to describe
|
|
283
|
+
# @return [String, nil] a sentence listing `command`'s own `ensures`, or nil if
|
|
284
|
+
# it declares none
|
|
198
285
|
def command_guarantees_sentence(command)
|
|
199
286
|
guarantees = command.ensures.map(&:description)
|
|
200
287
|
return nil if guarantees.empty?
|
|
@@ -202,6 +289,11 @@ module Hecks
|
|
|
202
289
|
"When it succeeds: #{guarantees.join('; ')}."
|
|
203
290
|
end
|
|
204
291
|
|
|
292
|
+
# Names what a command records.
|
|
293
|
+
#
|
|
294
|
+
# @param command [Bluebook::Command] the command to describe
|
|
295
|
+
# @return [String, nil] a sentence naming what `command` records, or nil if it
|
|
296
|
+
# emits nothing
|
|
205
297
|
def command_emits_sentence(command)
|
|
206
298
|
return nil if command.emits.empty?
|
|
207
299
|
|
|
@@ -214,6 +306,10 @@ module Hecks
|
|
|
214
306
|
# `given`s), but kept positive rather than phrased as a refusal
|
|
215
307
|
# reason. "Refused unless not X" is a sentence a reader has to
|
|
216
308
|
# invert in their head; "only goes through if X" is not.
|
|
309
|
+
#
|
|
310
|
+
# @param command [Bluebook::Command] the command to gather conditions for
|
|
311
|
+
# @param holder [Bluebook::Aggregate, Bluebook::Entity] `command`'s own holder
|
|
312
|
+
# @return [Array<String>] every required condition, stated positively
|
|
217
313
|
def conditions_of(command, holder)
|
|
218
314
|
conditions = []
|
|
219
315
|
|
|
@@ -234,6 +330,12 @@ module Hecks
|
|
|
234
330
|
|
|
235
331
|
# ── the reads ─────────────────────────────────────────────────────
|
|
236
332
|
|
|
333
|
+
# Narrates every query as one line per question.
|
|
334
|
+
#
|
|
335
|
+
# @param queries [Array<Bluebook::Query>] the queries to narrate
|
|
336
|
+
# @param holder_name [String] the queries' own holder's name
|
|
337
|
+
# @param depth [Integer] the heading level for the "Questions you can ask" section
|
|
338
|
+
# @return [String, nil] the section's Markdown prose, or nil if `queries` is empty
|
|
237
339
|
def queries_narrative(queries, holder_name, depth)
|
|
238
340
|
return nil if queries.empty?
|
|
239
341
|
|
|
@@ -257,12 +359,23 @@ module Hecks
|
|
|
257
359
|
"#{header}\n\n#{lines.join("\n")}"
|
|
258
360
|
end
|
|
259
361
|
|
|
362
|
+
# Translates a query comparator into plain English.
|
|
363
|
+
#
|
|
364
|
+
# @param comparator [String, Symbol] a where-clause comparator, such as `:eq`
|
|
365
|
+
# @return [String] the comparator in plain words, or `comparator.to_s` verbatim
|
|
366
|
+
# for one this file has no rendering rule for
|
|
260
367
|
def op_words(comparator)
|
|
261
368
|
{ eq: "is", lt: "under", lte: "at most", gt: "over", gte: "at least" }[comparator.to_s.to_sym] || comparator.to_s
|
|
262
369
|
end
|
|
263
370
|
|
|
264
371
|
# ── what happens on its own ───────────────────────────────────────
|
|
265
372
|
|
|
373
|
+
# Narrates what happens on its own: every policy and every saga.
|
|
374
|
+
#
|
|
375
|
+
# @param bluebook [Bluebook::Behaviour::Chapter] the chapter to narrate
|
|
376
|
+
# @param depth [Integer] the heading level for the "Reactions" section
|
|
377
|
+
# @return [String, nil] the section's Markdown prose, or nil if `bluebook`
|
|
378
|
+
# declares no policy and no process manager
|
|
266
379
|
def reactions_narrative(bluebook, depth)
|
|
267
380
|
return nil if bluebook.policies.empty? && bluebook.process_managers.empty?
|
|
268
381
|
|
|
@@ -286,8 +399,16 @@ module Hecks
|
|
|
286
399
|
|
|
287
400
|
# Both now live in `Naming` (a second projection, the glossary,
|
|
288
401
|
# needed them); kept here as names so this file reads as it did.
|
|
402
|
+
#
|
|
403
|
+
# @param items [Array<#to_s>] items to join, in order
|
|
404
|
+
# @param conj [String] conjunction placed before the last item
|
|
405
|
+
# @return [String] the joined sentence fragment, `""` for an empty `items`
|
|
289
406
|
def to_sentence_list(items, conj: "and") = Naming.to_sentence_list(items, conj: conj)
|
|
290
407
|
|
|
408
|
+
# Picks the English indefinite article for a word.
|
|
409
|
+
#
|
|
410
|
+
# @param word [String, Symbol] the word the article precedes
|
|
411
|
+
# @return [String] `"a"` or `"an"`
|
|
291
412
|
def a_or_an(word) = Naming.a_or_an(word)
|
|
292
413
|
end
|
|
293
414
|
end
|
|
@@ -55,11 +55,11 @@ module Hecks
|
|
|
55
55
|
#
|
|
56
56
|
# A capability says what a construct can do; this says what it must
|
|
57
57
|
# carry. `:vocabulary` needs a chapter declaring a Vocabulary
|
|
58
|
-
# aggregate, `:parser_table` one declaring Syntax —
|
|
59
|
-
#
|
|
60
|
-
# requirement
|
|
61
|
-
#
|
|
62
|
-
#
|
|
58
|
+
# aggregate, `:parser_table` one declaring Syntax — declared here
|
|
59
|
+
# rather than as a `raise` in the projection's own body, so the
|
|
60
|
+
# requirement is stated instead of written as behaviour. The
|
|
61
|
+
# registry refuses before the projection runs, so the projection
|
|
62
|
+
# itself no longer carries a guard about its own admission.
|
|
63
63
|
# `emits:` says what kind of artifact comes back.
|
|
64
64
|
#
|
|
65
65
|
# `:artifact` (the default) is one thing — a Hash, or a String.
|
|
@@ -70,24 +70,61 @@ module Hecks
|
|
|
70
70
|
# an ordinary Hash is guesswork — `{"name" => "Pizzas"}` is
|
|
71
71
|
# indistinguishable from a one-file tree — so `write` asks what the
|
|
72
72
|
# projection said rather than inspecting what it returned.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
73
|
+
# Registers `self` as a projection target under `key`, and stores the
|
|
74
|
+
# capability and aggregate requirements `admits!` later checks against.
|
|
75
|
+
#
|
|
76
|
+
# @param key [String, Symbol] the key to register `self` under (converted to a symbol)
|
|
77
|
+
# @param requires [Module, Array<Module>, nil] capability module(s) a construct must
|
|
78
|
+
# satisfy; nil or empty means `Bluebook::Behaviour::Chapter`
|
|
79
|
+
# @param declares [String, Symbol, Array<String, Symbol>, nil] aggregate name(s) the
|
|
80
|
+
# chapter must declare; nil means none
|
|
81
|
+
# @param emits [Symbol] the kind of artifact `self` returns: `:artifact` (the default,
|
|
82
|
+
# a single Hash or String) or `:files` (a path => contents tree)
|
|
83
|
+
# @param needs_world [Boolean] whether `self` is an export — a target that reads a
|
|
84
|
+
# domain's `.world`/`.hecksagon` bindings, not only its declaration. `false` (the
|
|
85
|
+
# default) for an ordinary projection; `true` refuses a `Projector.call` made with
|
|
86
|
+
# no `world:`.
|
|
87
|
+
# @return [Symbol] the registered key
|
|
88
|
+
def projects_as(key, requires: nil, declares: nil, emits: :artifact, needs_world: false)
|
|
89
|
+
@projection_emits = emits
|
|
90
|
+
@projection_key = key.to_sym
|
|
91
|
+
@projection_declares = Array(declares)
|
|
92
|
+
@projection_requires = Array(requires)
|
|
93
|
+
@projection_needs_world = needs_world
|
|
78
94
|
Projector.register(@projection_key, self)
|
|
79
95
|
@projection_key
|
|
80
96
|
end
|
|
81
97
|
|
|
98
|
+
# Gives the key this target registered under.
|
|
99
|
+
#
|
|
100
|
+
# @return [Symbol, nil] the key given to `projects_as`, or nil before it is called
|
|
82
101
|
def projection_key = @projection_key
|
|
83
102
|
|
|
84
103
|
# Empty means "a chapter" — resolved here rather than as a default
|
|
85
104
|
# argument, because Behaviour::Chapter is not loaded yet when this
|
|
86
105
|
# file is.
|
|
106
|
+
#
|
|
107
|
+
# @return [Array<String, Symbol>] aggregate names the chapter must declare; empty
|
|
108
|
+
# for none
|
|
87
109
|
def projection_declares = @projection_declares || []
|
|
88
110
|
|
|
111
|
+
# Gives the kind of artifact this target returns.
|
|
112
|
+
#
|
|
113
|
+
# @return [Symbol] the kind of artifact `self` returns: `:artifact` or `:files`
|
|
89
114
|
def projection_emits = @projection_emits || :artifact
|
|
90
115
|
|
|
116
|
+
# Tells whether this target is an export — one that needs a domain's
|
|
117
|
+
# `.world`/`.hecksagon` bindings, passed as `Projector.call`'s `world:`,
|
|
118
|
+
# rather than only its declaration.
|
|
119
|
+
#
|
|
120
|
+
# @return [Boolean] the value given to `projects_as`' `needs_world:`, or false
|
|
121
|
+
# before `projects_as` is called
|
|
122
|
+
def projection_needs_world? = @projection_needs_world || false
|
|
123
|
+
|
|
124
|
+
# Names the capability module(s) a construct must satisfy, defaulting
|
|
125
|
+
# to plain chapter-hood when `projects_as` named none.
|
|
126
|
+
#
|
|
127
|
+
# @return [Array<Module>] required capability modules
|
|
91
128
|
def projection_requires
|
|
92
129
|
req = @projection_requires
|
|
93
130
|
req.nil? || req.empty? ? [Bluebook::Behaviour::Chapter] : req
|
data/lib/hecks/projector.rb
CHANGED
|
@@ -17,8 +17,11 @@ module Hecks
|
|
|
17
17
|
# every real projection target (Rust/UL/OIDC all project one domain at
|
|
18
18
|
# a time), and deliberately narrower than `Exporter.call`'s own
|
|
19
19
|
# multi-domain shape.
|
|
20
|
-
#
|
|
21
|
-
#
|
|
20
|
+
#
|
|
21
|
+
# ## Three kinds of "project"
|
|
22
|
+
#
|
|
23
|
+
# Told apart by what they need as input. Only the first belongs in this
|
|
24
|
+
# registry.
|
|
22
25
|
#
|
|
23
26
|
# a projection takes a chapter's declaration and answers something
|
|
24
27
|
# that describes the domain: its IR, its storage shape, an OIDC scope
|
|
@@ -28,11 +31,13 @@ module Hecks
|
|
|
28
31
|
#
|
|
29
32
|
# an export takes a declaration and its bindings and answers
|
|
30
33
|
# something that is the domain, running elsewhere — rust/project.rb's
|
|
31
|
-
# generated crate, the WASM artifact, the
|
|
32
|
-
#
|
|
33
|
-
# projection never looks at, because a
|
|
34
|
-
# it is wired.
|
|
35
|
-
# this
|
|
34
|
+
# generated crate, the WASM artifact, the CloudFormation template
|
|
35
|
+
# `lib/hecks/projections/deploy` renders. It needs the
|
|
36
|
+
# `.world`/`.hecksagon` a projection never looks at, because a
|
|
37
|
+
# running system has to know how it is wired. A target that needs
|
|
38
|
+
# this declares `needs_world: true` (`Target#projects_as`) and reads
|
|
39
|
+
# `options.fetch(:world)`; `Projector.call`'s own `world:` keyword is
|
|
40
|
+
# what carries it, merged into `options` before the target ever runs.
|
|
36
41
|
#
|
|
37
42
|
# a state projection takes records — a domain after dispatch — and is
|
|
38
43
|
# a read-model question wearing the same word.
|
|
@@ -42,8 +47,10 @@ module Hecks
|
|
|
42
47
|
# dispatches. Converting it into this registry would be a category
|
|
43
48
|
# error, however much its name suggests otherwise.
|
|
44
49
|
#
|
|
45
|
-
#
|
|
46
|
-
#
|
|
50
|
+
# ## One word, three other meanings
|
|
51
|
+
#
|
|
52
|
+
# Worth naming too, because grepping "projection" turns all of these up
|
|
53
|
+
# and none is the above:
|
|
47
54
|
#
|
|
48
55
|
# Ports::Projection read-model catch-up, events folded into state
|
|
49
56
|
# bin/project forces that catch-up by hand
|
|
@@ -61,24 +68,48 @@ module Hecks
|
|
|
61
68
|
# deliberately unguarded: a spec re-registering a stub under the same
|
|
62
69
|
# name between examples is the ordinary case, not a footgun to fence
|
|
63
70
|
# against.
|
|
71
|
+
# Adds `projector` to the registry under `name`, replacing anything
|
|
72
|
+
# already registered there.
|
|
73
|
+
#
|
|
74
|
+
# @param name [String, Symbol] the key `projector` is looked up by (converted to a symbol)
|
|
75
|
+
# @param projector [Module, Class, #call] anything answering `call(bluebook:, options:)`
|
|
76
|
+
# @return [void]
|
|
64
77
|
def register(name, projector)
|
|
65
78
|
registry[name.to_sym] = projector
|
|
66
79
|
end
|
|
67
80
|
|
|
81
|
+
# Runs the projector registered under `name` against `bluebook`,
|
|
82
|
+
# after refusing a construct it does not admit.
|
|
83
|
+
#
|
|
68
84
|
# `bluebook:` is kept as the keyword because it is the shipped
|
|
69
85
|
# spelling and every existing caller uses it — but what it accepts is
|
|
70
86
|
# any construct that emits IR, and `admits!` is what decides whether
|
|
71
87
|
# this target can actually take the one handed over.
|
|
72
|
-
|
|
88
|
+
#
|
|
89
|
+
# @param name [String, Symbol] the registered projector's key
|
|
90
|
+
# @param bluebook [Bluebook::Behaviour::Chapter, Hecks::IR] the chapter or
|
|
91
|
+
# IR-emitting construct to project
|
|
92
|
+
# @param options [Hash] projector-specific options, passed through unchanged
|
|
93
|
+
# @param world [Bluebook::World, nil] the domain's `.world`/`.hecksagon` bindings,
|
|
94
|
+
# required by an export (`needs_world: true`); nil for an ordinary projection.
|
|
95
|
+
# Merged into `options[:world]` before the target runs — the target never
|
|
96
|
+
# receives it as a separate argument.
|
|
97
|
+
# @return [Object] whatever the projector's own `call` returns: typically a
|
|
98
|
+
# `Hash`/`String` artifact, or a `Hash{String => String}` file tree
|
|
99
|
+
# @raise [UnknownProjector] if no projector is registered under `name`
|
|
100
|
+
# @raise [WrongConstruct] if `bluebook` lacks a capability or aggregate the
|
|
101
|
+
# projector requires, or if the projector needs `world:` and none is given
|
|
102
|
+
def call(name, bluebook:, options: {}, world: nil)
|
|
73
103
|
projector = registry.fetch(name.to_sym) do
|
|
74
104
|
raise UnknownProjector, "no projector registered for #{name.inspect} — registered: #{registered.sort.inspect}"
|
|
75
105
|
end
|
|
76
|
-
admits!(name, projector, bluebook)
|
|
77
|
-
projector.call(bluebook: bluebook, options: options)
|
|
106
|
+
admits!(name, projector, bluebook, world)
|
|
107
|
+
projector.call(bluebook: bluebook, options: world ? options.merge(world: world) : options)
|
|
78
108
|
end
|
|
79
109
|
|
|
80
|
-
#
|
|
81
|
-
#
|
|
110
|
+
# Refuses `construct` if it lacks a capability or declared aggregate
|
|
111
|
+
# `projector` requires. A projection names the capabilities it needs;
|
|
112
|
+
# this is what enforces that, before the projector runs.
|
|
82
113
|
#
|
|
83
114
|
# One check covers both shapes. An ordinary construct includes its
|
|
84
115
|
# capabilities and a class-shaped one — Command, Entity, ValueObject
|
|
@@ -87,7 +118,21 @@ module Hecks
|
|
|
87
118
|
# started as two checks on the assumption it would not; a spec
|
|
88
119
|
# asserting the assumption failed, which is the only reason the
|
|
89
120
|
# redundant half was noticed.
|
|
90
|
-
|
|
121
|
+
#
|
|
122
|
+
# @param name [String, Symbol] the projector's registered key, used in the message
|
|
123
|
+
# when refusing
|
|
124
|
+
# @param projector [Module, Class, #call] the target being checked; consulted for
|
|
125
|
+
# `projection_requires`, `projection_declares`, and `projection_needs_world?` when
|
|
126
|
+
# it answers them
|
|
127
|
+
# @param construct [Bluebook::Behaviour::Chapter, Hecks::IR] the chapter or
|
|
128
|
+
# IR-emitting construct offered to the projector
|
|
129
|
+
# @param world [Bluebook::World, nil] the `world:` given to `Projector.call`, checked
|
|
130
|
+
# against the target's own `needs_world:` declaration
|
|
131
|
+
# @return [void]
|
|
132
|
+
# @raise [WrongConstruct] if `construct` lacks a required capability, the chapter it
|
|
133
|
+
# is declares no aggregate the projector needs, or the projector needs `world:`
|
|
134
|
+
# and `world` is nil
|
|
135
|
+
def admits!(name, projector, construct, world = nil)
|
|
91
136
|
needed = projector.respond_to?(:projection_requires) ? projector.projection_requires : []
|
|
92
137
|
missing = needed.reject { |capability| capable?(construct, capability) }
|
|
93
138
|
unless missing.empty?
|
|
@@ -98,25 +143,49 @@ module Hecks
|
|
|
98
143
|
|
|
99
144
|
declared = projector.respond_to?(:projection_declares) ? projector.projection_declares : []
|
|
100
145
|
absent = declared.reject { |named| construct.aggregate(named) }
|
|
101
|
-
|
|
146
|
+
unless absent.empty?
|
|
147
|
+
raise WrongConstruct,
|
|
148
|
+
"#{name.inspect} needs a chapter declaring #{absent.join(' and ')}; " \
|
|
149
|
+
"#{construct.name} declares no such aggregate."
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
return unless projector.respond_to?(:projection_needs_world?) && projector.projection_needs_world? && world.nil?
|
|
102
153
|
|
|
103
|
-
raise WrongConstruct,
|
|
104
|
-
"#{name.inspect} needs a chapter declaring #{absent.join(' and ')}; " \
|
|
105
|
-
"#{construct.name} declares no such aggregate."
|
|
154
|
+
raise WrongConstruct, "#{name.inspect} needs .world/.hecksagon bindings — pass world: to Projector.call."
|
|
106
155
|
end
|
|
107
156
|
|
|
157
|
+
# Tells whether `construct` has the capability `admits!` requires of it.
|
|
158
|
+
#
|
|
159
|
+
# @param construct [Bluebook::Behaviour::Chapter, Hecks::IR] the construct to check
|
|
160
|
+
# @param capability [Module] the capability module to check for
|
|
161
|
+
# @return [Boolean] true if `construct` is a `capability`
|
|
108
162
|
def capable?(construct, capability) = construct.is_a?(capability)
|
|
109
163
|
|
|
110
164
|
# What kind of artifact a registered target emits — asked of the
|
|
111
165
|
# projection rather than inferred from what it returned.
|
|
166
|
+
#
|
|
167
|
+
# @param name [String, Symbol] the registered projector's key
|
|
168
|
+
# @return [Symbol] `:files` for a path => contents tree, `:artifact` (the
|
|
169
|
+
# default, including for an unregistered `name`) for a single Hash or String
|
|
112
170
|
def emits_for(name)
|
|
113
171
|
projector = registry.fetch(name.to_sym) { return :artifact }
|
|
114
172
|
projector.respond_to?(:projection_emits) ? projector.projection_emits : :artifact
|
|
115
173
|
end
|
|
116
174
|
|
|
175
|
+
# Tells whether a projector is registered under `name`.
|
|
176
|
+
#
|
|
177
|
+
# @param name [String, Symbol] the key to look up
|
|
178
|
+
# @return [Boolean] true if a projector is registered under `name`
|
|
117
179
|
def registered?(name) = registry.key?(name.to_sym)
|
|
180
|
+
|
|
181
|
+
# Lists every key currently registered.
|
|
182
|
+
#
|
|
183
|
+
# @return [Array<Symbol>] every key currently registered
|
|
118
184
|
def registered = registry.keys
|
|
119
185
|
|
|
186
|
+
# Gives the live registry, initializing it on first use.
|
|
187
|
+
#
|
|
188
|
+
# @return [Hash{Symbol => Module, Class, #call}] the live key => projector registry
|
|
120
189
|
def registry
|
|
121
190
|
@registry ||= {}
|
|
122
191
|
end
|
|
@@ -126,6 +195,10 @@ module Hecks
|
|
|
126
195
|
# (`:oidc`). Both resolve here, so the constant form is added
|
|
127
196
|
# surface rather than a replacement — every `Projector.call(:ir, ...)`
|
|
128
197
|
# written before this existed keeps working untouched.
|
|
198
|
+
#
|
|
199
|
+
# @param target [String, Symbol, #projection_key] a registered key, or a target
|
|
200
|
+
# that declared its own key with `Target#projects_as`
|
|
201
|
+
# @return [String, Symbol] the key to call the target under
|
|
129
202
|
def key_for(target)
|
|
130
203
|
return target.projection_key if target.respond_to?(:projection_key) && target.projection_key
|
|
131
204
|
|
|
@@ -141,6 +214,11 @@ module Hecks
|
|
|
141
214
|
# than from inspecting the artifact. A Hash of path => contents and a
|
|
142
215
|
# Hash that simply happens to hold strings are the same object to
|
|
143
216
|
# Ruby; only the projection knows which it meant.
|
|
217
|
+
#
|
|
218
|
+
# @param artifact [Hash, String] the projector's output; a file tree when `as: :files`
|
|
219
|
+
# @param out [String] the path to write to; a directory when `as: :files`
|
|
220
|
+
# @param as [Symbol] `:files` to write a tree, anything else to write one file
|
|
221
|
+
# @return [String, Array<String>] the path written, or every path written when `as: :files`
|
|
144
222
|
def write(artifact, out, as: :artifact)
|
|
145
223
|
return write_tree(artifact, out) if as == :files
|
|
146
224
|
|
|
@@ -150,6 +228,10 @@ module Hecks
|
|
|
150
228
|
|
|
151
229
|
# Answers the paths written, in the order given — so a caller can
|
|
152
230
|
# report what happened without re-deriving it from the tree.
|
|
231
|
+
#
|
|
232
|
+
# @param files [Hash{String => String}] relative path => contents
|
|
233
|
+
# @param directory [String] the directory to write the tree under
|
|
234
|
+
# @return [Array<String>] each file's full written path, in `files`' order
|
|
153
235
|
def write_tree(files, directory)
|
|
154
236
|
require "fileutils"
|
|
155
237
|
files.map do |relative, contents|
|