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
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
Hecks.bluebook "Compliance" do
|
|
2
|
+
vision "Something elsewhere already acted to contain a risk; this domain tracks the human review that decides what happens next."
|
|
3
|
+
supporting
|
|
4
|
+
|
|
5
|
+
# LIVES HERE (lib/hecks/framework/bluebook/), NOT under some
|
|
6
|
+
# domain's own examples/ directory, but that's a statement about
|
|
7
|
+
# SOURCE SHARING, not about how it's consumed — unlike Governance/
|
|
8
|
+
# Identity (which every real attaching domain only ever wants IN-
|
|
9
|
+
# PROCESS), Compliance genuinely supports BOTH modes from the same
|
|
10
|
+
# source: a domain that wants Review handled locally can
|
|
11
|
+
# `uses_framework "Compliance"` exactly like those two; a domain that
|
|
12
|
+
# wants it as a real, separately-deployed service — Banking's own
|
|
13
|
+
# choice, examples/compliance/ — reaches it through `across
|
|
14
|
+
# "Compliance"` on a policy instead, genuine cross-Lambda delivery.
|
|
15
|
+
# Nothing about this bluebook's own declarations changes between the
|
|
16
|
+
# two; only whether the CONSUMER merges it in or deploys it apart.
|
|
17
|
+
#
|
|
18
|
+
# examples/compliance/bluebook/compliance.bluebook is a SYMLINK back
|
|
19
|
+
# to this file (not the other way around, as it briefly was in
|
|
20
|
+
# 1.5.0) — a symlink pointing outside lib/ never survives `gem
|
|
21
|
+
# build` (RubyGems drops it, warning "not supported on all
|
|
22
|
+
# platforms"), so the real content has to live in the tree the
|
|
23
|
+
# gemspec actually packages.
|
|
24
|
+
#
|
|
25
|
+
# CONTAIN FIRST, INVESTIGATE SECOND — the real pattern this domain
|
|
26
|
+
# exists for. Banking's own "Compliance officer" role already acts
|
|
27
|
+
# directly and synchronously in three places (Customer.Suspend/
|
|
28
|
+
# Reinstate, Account.Freeze/Unfreeze, OnboardingCase.Clear/Decline) —
|
|
29
|
+
# this domain is deliberately NOT a fourth. By the time either
|
|
30
|
+
# aggregate below is opened, the containing action (a freeze, a box
|
|
31
|
+
# surrender) has ALREADY happened, unilaterally, elsewhere; this
|
|
32
|
+
# domain never asks permission, it is TOLD a fact and gives the same
|
|
33
|
+
# officer a place to record what they decided about it afterward.
|
|
34
|
+
#
|
|
35
|
+
# TWO AGGREGATES, NOT ONE — found live, designing this: Banking's own
|
|
36
|
+
# `ReviewOnFreeze`/`ReviewOnBoxSurrender` policies forward their
|
|
37
|
+
# triggering event's payload VERBATIM (neither declares a `with:`
|
|
38
|
+
# projection, which policies have gained since), and the two events
|
|
39
|
+
# carry genuinely different fields (`AccountFrozen`: `number`;
|
|
40
|
+
# `BoxSurrendered`: `branch_code` + `box_number`). One shared
|
|
41
|
+
# aggregate would need an artificial union of both shapes — and while
|
|
42
|
+
# `with:` could now reshape either side into it, that would be
|
|
43
|
+
# inventing a sameness the domain does not have. Two small aggregates,
|
|
44
|
+
# each identified by its own subject's own natural key, need neither.
|
|
45
|
+
|
|
46
|
+
aggregate "AccountFreezeReview" do
|
|
47
|
+
description "The compliance review a frozen account gets, opened the moment Banking::Account.Freeze already happened."
|
|
48
|
+
|
|
49
|
+
attribute :number, AccountNumber
|
|
50
|
+
|
|
51
|
+
identified_by :number
|
|
52
|
+
|
|
53
|
+
value_object "AccountNumber" do
|
|
54
|
+
attribute :value, String
|
|
55
|
+
invariant("an account number is present") { !value.to_s.empty? }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
lifecycle :status, default: "open" do
|
|
59
|
+
transition "Clear" => "cleared", from: "open"
|
|
60
|
+
transition "Escalate" => "escalated", from: "open"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# SYSTEM-TRIGGERED, matching `AccountFrozen`'s own payload shape
|
|
64
|
+
# field for field — `Banking::Account.AccountFrozen`'s `number` is
|
|
65
|
+
# the ONLY field this ever receives (ReviewOnFreeze's own `on
|
|
66
|
+
# "Account.AccountFrozen"`), so this declares exactly that and
|
|
67
|
+
# nothing more; an unrecognized field would refuse at the JSON
|
|
68
|
+
# boundary rather than silently drop it.
|
|
69
|
+
command "Open" do
|
|
70
|
+
role "System"
|
|
71
|
+
goal "Register that a frozen account needs compliance review"
|
|
72
|
+
|
|
73
|
+
sets :number
|
|
74
|
+
|
|
75
|
+
emits AccountFreezeReviewOpened
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
command "Clear" do
|
|
79
|
+
role "Compliance officer"
|
|
80
|
+
goal "Confirm the freeze was warranted, or resolve it, with nothing further to escalate"
|
|
81
|
+
|
|
82
|
+
reference_to AccountFreezeReview
|
|
83
|
+
|
|
84
|
+
emits AccountFreezeReviewCleared
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
command "Escalate" do
|
|
88
|
+
role "Compliance officer"
|
|
89
|
+
goal "Send a genuine finding on to whatever handles it next — a formal filing, a longer hold, a referral"
|
|
90
|
+
|
|
91
|
+
reference_to AccountFreezeReview
|
|
92
|
+
|
|
93
|
+
emits AccountFreezeReviewEscalated
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
aggregate "BoxSurrenderReview" do
|
|
98
|
+
description "The compliance review a surrendered safe deposit box gets — audit/escheatment implications, not a fraud contain the way a freeze is, but the same after-the-fact review shape."
|
|
99
|
+
|
|
100
|
+
attribute :branch_code, BranchCode
|
|
101
|
+
attribute :box_number, BoxNumber
|
|
102
|
+
|
|
103
|
+
identified_by :branch_code, :box_number
|
|
104
|
+
|
|
105
|
+
value_object "BranchCode" do
|
|
106
|
+
attribute :value, String
|
|
107
|
+
invariant("a branch is coded") { !value.to_s.empty? }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
value_object "BoxNumber" do
|
|
111
|
+
attribute :value, Integer
|
|
112
|
+
invariant("a box is numbered from one") { value.positive? }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
lifecycle :status, default: "open" do
|
|
116
|
+
transition "Clear" => "cleared", from: "open"
|
|
117
|
+
transition "Escalate" => "escalated", from: "open"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Matches `BoxSurrendered`'s own payload field for field
|
|
121
|
+
# (`ReviewOnBoxSurrender`'s own `on SafeDepositBox::BoxSurrendered`) — the same
|
|
122
|
+
# discipline `AccountFreezeReview.Open` holds itself to, above.
|
|
123
|
+
command "Open" do
|
|
124
|
+
role "System"
|
|
125
|
+
goal "Register that a surrendered box needs compliance review"
|
|
126
|
+
|
|
127
|
+
sets :branch_code
|
|
128
|
+
sets :box_number
|
|
129
|
+
|
|
130
|
+
emits BoxSurrenderReviewOpened
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
command "Clear" do
|
|
134
|
+
role "Compliance officer"
|
|
135
|
+
goal "Confirm the surrender needs no further compliance action"
|
|
136
|
+
|
|
137
|
+
reference_to BoxSurrenderReview
|
|
138
|
+
|
|
139
|
+
emits BoxSurrenderReviewCleared
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
command "Escalate" do
|
|
143
|
+
role "Compliance officer"
|
|
144
|
+
goal "Send a genuine finding on to whatever handles it next"
|
|
145
|
+
|
|
146
|
+
reference_to BoxSurrenderReview
|
|
147
|
+
|
|
148
|
+
emits BoxSurrenderReviewEscalated
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# A THIRD SHAPE, NOT A UNION OF THE OTHER TWO — the same restraint the
|
|
153
|
+
# header above holds Banking's own two events to. `Privacy::Marking`'s
|
|
154
|
+
# own `Marked` event carries `domain`/`attribute_path`, matching
|
|
155
|
+
# neither `AccountFrozen` (`number`) nor `BoxSurrendered`
|
|
156
|
+
# (`branch_code`/`box_number`), so this gets its own aggregate rather
|
|
157
|
+
# than an artificial union.
|
|
158
|
+
#
|
|
159
|
+
# REACHED VIA `translates`, NOT a `policy` this bluebook declares —
|
|
160
|
+
# which foreign domain's event a consumer conforms to is a wiring
|
|
161
|
+
# decision (see `HecksagonBuilder#translates`'s own header), so the
|
|
162
|
+
# reaction lives in whichever `.hecksagon` attaches both Privacy and
|
|
163
|
+
# Compliance, not here.
|
|
164
|
+
aggregate "PrivacyReview" do
|
|
165
|
+
description "The compliance review a newly marked sensitive field gets, opened the moment Privacy::Marking.Mark already happened."
|
|
166
|
+
|
|
167
|
+
attribute :domain, Domain
|
|
168
|
+
attribute :attribute_path, AttributePath
|
|
169
|
+
|
|
170
|
+
identified_by :domain, :attribute_path
|
|
171
|
+
|
|
172
|
+
value_object "Domain" do
|
|
173
|
+
attribute :value, String
|
|
174
|
+
invariant("a review names the domain whose attribute was marked") { !value.to_s.empty? }
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
value_object "AttributePath" do
|
|
178
|
+
attribute :value, String
|
|
179
|
+
invariant("a review names the attribute that was marked") { !value.to_s.empty? }
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
lifecycle :status, default: "open" do
|
|
183
|
+
transition "Clear" => "cleared", from: "open"
|
|
184
|
+
transition "Escalate" => "escalated", from: "open"
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# SYSTEM-TRIGGERED, matching only `Privacy::Marking`'s own identity
|
|
188
|
+
# fields — the `translates` block that reacts to `Marked` forwards
|
|
189
|
+
# just `domain`/`attribute_path`, the same restraint
|
|
190
|
+
# `AccountFreezeReview.Open`/`BoxSurrenderReview.Open` already hold
|
|
191
|
+
# themselves to above; `category`/`readable_by` stay on the
|
|
192
|
+
# marking itself, not duplicated here.
|
|
193
|
+
command "Open" do
|
|
194
|
+
role "System"
|
|
195
|
+
goal "Register that a newly marked sensitive field needs compliance review"
|
|
196
|
+
|
|
197
|
+
sets :domain
|
|
198
|
+
sets :attribute_path
|
|
199
|
+
|
|
200
|
+
emits PrivacyReviewOpened
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
command "Clear" do
|
|
204
|
+
role "Compliance officer"
|
|
205
|
+
goal "Confirm the marking was warranted, or resolve it, with nothing further to escalate"
|
|
206
|
+
|
|
207
|
+
reference_to PrivacyReview
|
|
208
|
+
|
|
209
|
+
emits PrivacyReviewCleared
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
command "Escalate" do
|
|
213
|
+
role "Compliance officer"
|
|
214
|
+
goal "Send a genuine finding on to whatever handles it next"
|
|
215
|
+
|
|
216
|
+
reference_to PrivacyReview
|
|
217
|
+
|
|
218
|
+
emits PrivacyReviewEscalated
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
Hecks.bluebook "Privacy" do
|
|
2
|
+
vision "Which fields of a domain's own aggregates are sensitive is a fact about that domain, declared apart from it — the domain itself never spells out that one of its own attributes is PII or PHI."
|
|
3
|
+
supporting
|
|
4
|
+
|
|
5
|
+
# A DOMAIN ATTACHING THIS NEVER MARKS ITSELF — `Mark`'s `role
|
|
6
|
+
# "Deployment"` matches every other wiring-time command in this
|
|
7
|
+
# codebase (`Hecksagon::UsesFramework`, `Tenancy::Tenant.Register`):
|
|
8
|
+
# something outside the domain's own bluebook decides a field is
|
|
9
|
+
# sensitive, the same way persistence binds are a wiring decision
|
|
10
|
+
# and not baked into an aggregate's own declaration.
|
|
11
|
+
aggregate "Marking" do
|
|
12
|
+
description "One attribute of one domain's own aggregate, flagged sensitive — and the role required to read it unredacted."
|
|
13
|
+
|
|
14
|
+
attribute :domain, MarkingDomain
|
|
15
|
+
attribute :attribute_path, AttributePath
|
|
16
|
+
attribute :category, Category
|
|
17
|
+
attribute :readable_by, RoleName
|
|
18
|
+
|
|
19
|
+
identified_by :domain, :attribute_path
|
|
20
|
+
|
|
21
|
+
value_object "MarkingDomain" do
|
|
22
|
+
attribute :value, String
|
|
23
|
+
invariant("a marking names the domain it applies to") { !value.to_s.empty? }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# DOTTED, same convention Forms' own `Field#path` and a query's
|
|
27
|
+
# cross-object `where` clause already use — `"attendee.medications"`,
|
|
28
|
+
# not a bare leaf name, so a marking can reach into a nested value
|
|
29
|
+
# object rather than only a top-level attribute.
|
|
30
|
+
value_object "AttributePath" do
|
|
31
|
+
attribute :value, String
|
|
32
|
+
invariant("a marking names an attribute path") { !value.to_s.empty? }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# OPEN-ENDED ON PURPOSE — "pii", "phi", whatever else a consuming
|
|
36
|
+
# domain finds itself needing, none of it enumerated here. This
|
|
37
|
+
# bluebook is the mechanism (mark, gate, redact); it does not also
|
|
38
|
+
# own the vocabulary of sensitivity categories that exist in the
|
|
39
|
+
# world.
|
|
40
|
+
value_object "Category" do
|
|
41
|
+
attribute :value, String
|
|
42
|
+
invariant("a marking is categorized") { !value.to_s.empty? }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
value_object "RoleName" do
|
|
46
|
+
attribute :value, String
|
|
47
|
+
invariant("reading a marked field unredacted requires a named role") { !value.to_s.empty? }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
command "Mark" do
|
|
51
|
+
role "Deployment"
|
|
52
|
+
goal "Flag one attribute of a domain's own aggregate as sensitive, gating who may read it unredacted"
|
|
53
|
+
|
|
54
|
+
attribute :domain, MarkingDomain
|
|
55
|
+
attribute :attribute_path, AttributePath
|
|
56
|
+
attribute :category, Category
|
|
57
|
+
attribute :readable_by, RoleName
|
|
58
|
+
|
|
59
|
+
emits Marked
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
command "Unmark" do
|
|
63
|
+
role "Deployment"
|
|
64
|
+
goal "Remove a sensitivity flag from an attribute"
|
|
65
|
+
|
|
66
|
+
reference_to Marking
|
|
67
|
+
|
|
68
|
+
emits Unmarked
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
query "ForDomain" do
|
|
72
|
+
description "Every marking declared for one domain, read by whatever gates or redacts that domain's own records."
|
|
73
|
+
attribute :domain, MarkingDomain
|
|
74
|
+
where(domain: :domain)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# **Cryptoshredding** — a right-to-erasure request satisfied without
|
|
79
|
+
# rewriting or deleting a single event. `key_reference` is an opaque
|
|
80
|
+
# handle a caller obtains from `Ports::KeyVault.issue` before
|
|
81
|
+
# dispatching `Issue`, the same "nothing is minted inside a bluebook"
|
|
82
|
+
# convention `Identity::Register`'s own `identity_id` and
|
|
83
|
+
# `Delivery::Record`'s own `message_id` already hold to (the
|
|
84
|
+
# expression sublanguage cannot call a port at all). The key material
|
|
85
|
+
# itself never appears here or on any emitted event — only the fact
|
|
86
|
+
# that a reference exists, and the fact that it was destroyed.
|
|
87
|
+
aggregate "SubjectKey" do
|
|
88
|
+
description "One data subject's own encryption key, from the moment it exists to the moment it is destroyed."
|
|
89
|
+
|
|
90
|
+
attribute :domain, KeyDomain
|
|
91
|
+
attribute :subject_id, SubjectId
|
|
92
|
+
attribute :key_reference, KeyReference
|
|
93
|
+
|
|
94
|
+
identified_by :domain, :subject_id
|
|
95
|
+
|
|
96
|
+
value_object "KeyDomain" do
|
|
97
|
+
attribute :value, String
|
|
98
|
+
invariant("a key names the domain its subject belongs to") { !value.to_s.empty? }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
value_object "SubjectId" do
|
|
102
|
+
attribute :value, String
|
|
103
|
+
invariant("a key names the subject it was issued for") { !value.to_s.empty? }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
value_object "KeyReference" do
|
|
107
|
+
attribute :value, String
|
|
108
|
+
invariant("a key carries the vault's own opaque reference to it") { !value.to_s.empty? }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# `Shred`'s own guard against running twice — the same mechanism
|
|
112
|
+
# `Compliance::PrivacyReview`'s `Clear`/`Escalate` already use rather
|
|
113
|
+
# than an optional timestamp plus a hand-written `given`.
|
|
114
|
+
lifecycle :status, default: "active" do
|
|
115
|
+
transition "Shred" => "shredded", from: "active"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
command "Issue" do
|
|
119
|
+
role "System"
|
|
120
|
+
goal "Record that a subject's data is now encrypted under a fresh, dedicated key"
|
|
121
|
+
|
|
122
|
+
attribute :domain, KeyDomain
|
|
123
|
+
attribute :subject_id, SubjectId
|
|
124
|
+
attribute :key_reference, KeyReference
|
|
125
|
+
|
|
126
|
+
sets :domain
|
|
127
|
+
sets :subject_id
|
|
128
|
+
sets :key_reference
|
|
129
|
+
|
|
130
|
+
emits SubjectKeyIssued
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# **Records destruction, never performs it** — the actual
|
|
134
|
+
# `Ports::KeyVault.destroy` call happens in driving-side code before
|
|
135
|
+
# this is ever dispatched, the same boundary `send_issue.rb` draws
|
|
136
|
+
# around `Delivery::Record` for an already-sent email. This
|
|
137
|
+
# command's own job is to make an already-irreversible fact
|
|
138
|
+
# durable, not to cause it.
|
|
139
|
+
command "Shred" do
|
|
140
|
+
role "Privacy officer"
|
|
141
|
+
goal "Mark this subject's key destroyed, so its ciphertext is recorded as permanently unrecoverable"
|
|
142
|
+
|
|
143
|
+
reference_to SubjectKey
|
|
144
|
+
|
|
145
|
+
emits SubjectKeyShredded
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
query "ForSubject" do
|
|
149
|
+
description "One subject's key record, checked before any attempt to decrypt their sensitive fields."
|
|
150
|
+
attribute :domain, KeyDomain
|
|
151
|
+
attribute :subject_id, SubjectId
|
|
152
|
+
where(domain: :domain, subject_id: :subject_id)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
@@ -30,6 +30,21 @@
|
|
|
30
30
|
"scope": "compliance:box_surrender_review.open",
|
|
31
31
|
"verb": "Compliance::BoxSurrenderReview.Open",
|
|
32
32
|
"role": "System"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"scope": "compliance:privacy_review.clear",
|
|
36
|
+
"verb": "Compliance::PrivacyReview.Clear",
|
|
37
|
+
"role": "Compliance officer"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"scope": "compliance:privacy_review.escalate",
|
|
41
|
+
"verb": "Compliance::PrivacyReview.Escalate",
|
|
42
|
+
"role": "Compliance officer"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"scope": "compliance:privacy_review.open",
|
|
46
|
+
"verb": "Compliance::PrivacyReview.Open",
|
|
47
|
+
"role": "System"
|
|
33
48
|
}
|
|
34
49
|
],
|
|
35
50
|
"roles": [
|
data/lib/hecks/framework.rb
CHANGED
|
@@ -6,6 +6,8 @@ module Hecks
|
|
|
6
6
|
# domain-agnostic chapters no single example owns, shared by reference
|
|
7
7
|
# rather than copied into every domain that wants one.
|
|
8
8
|
#
|
|
9
|
+
# ## Where it lives
|
|
10
|
+
#
|
|
9
11
|
# Under `lib/`, not a top-level sibling — a real consumer (embryonaut)
|
|
10
12
|
# vendors only `lib/` (`bin/vendor-hecks`, and this gem's own
|
|
11
13
|
# `hecks.gemspec`, both glob `lib/**/*`), so a `framework/`
|
|
@@ -19,6 +21,8 @@ module Hecks
|
|
|
19
21
|
# non-Ruby data a module owns lives inside `lib/` with the code that
|
|
20
22
|
# reads it, not beside it.
|
|
21
23
|
#
|
|
24
|
+
# ## Derived, not hand-listed
|
|
25
|
+
#
|
|
22
26
|
# Derived from the directory, not hand-listed a second time — the same
|
|
23
27
|
# reasoning `Assembly::CONTRACTS` gives for reading the language's own
|
|
24
28
|
# fields from a table instead of restating them: a member added here
|
|
@@ -27,19 +31,13 @@ module Hecks
|
|
|
27
31
|
# stale in silence. `spec/corpus_spec.rb`'s own `FRAMEWORK_MEMBERS`
|
|
28
32
|
# glob is the precedent this mirrors.
|
|
29
33
|
#
|
|
34
|
+
# ## Naming
|
|
35
|
+
#
|
|
30
36
|
# Named by file stem, capitalized — `governance.bluebook` holds
|
|
31
37
|
# `Hecks.bluebook "Governance"`, the same one-to-one spelling every
|
|
32
38
|
# other chapter in this codebase already keeps between its filename
|
|
33
39
|
# and its declared name.
|
|
34
40
|
module Framework
|
|
35
|
-
ROOT = File.expand_path("framework/bluebook", __dir__).freeze
|
|
36
|
-
|
|
37
|
-
def self.members
|
|
38
|
-
Dir.glob(File.join(ROOT, "*.bluebook")).to_h do |path|
|
|
39
|
-
[Naming.pascal(File.basename(path, ".bluebook")), path]
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
|
|
43
41
|
# Loaded from its own real path, always — never a copy. A domain
|
|
44
42
|
# booted through `Fuzzing::IsolatedBoot`'s tmp-directory copy still
|
|
45
43
|
# reaches the same `framework/bluebook/governance.bluebook` this
|
|
@@ -64,28 +62,36 @@ module Hecks
|
|
|
64
62
|
# everywhere else too. See `examples/banking/bluebook/banking.hecksagon`
|
|
65
63
|
# for the pattern — a real `.hecksagon` file can hold more than one
|
|
66
64
|
# `Hecks.hecksagon` call, one per domain it wires.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
#
|
|
70
|
-
#
|
|
71
|
-
#
|
|
72
|
-
#
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
65
|
+
ROOT = File.expand_path("framework/bluebook", __dir__).freeze
|
|
66
|
+
|
|
67
|
+
# Every framework member available to attach, by declared name.
|
|
68
|
+
#
|
|
69
|
+
# @return [Hash{String => String}] each member's capitalized bluebook name,
|
|
70
|
+
# mapped to the absolute path of its `.bluebook` file
|
|
71
|
+
def self.members
|
|
72
|
+
Dir.glob(File.join(ROOT, "*.bluebook")).to_h do |path|
|
|
73
|
+
[Naming.pascal(File.basename(path, ".bluebook")), path]
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
79
77
|
# Every member whose own bluebook declares `provides capability` —
|
|
80
78
|
# read off the member's real IR (each loaded into a scratch registry,
|
|
81
79
|
# never the caller's), not off its name. Used where a check needs to
|
|
82
80
|
# say which member would satisfy it (`refuse_ungoverned_roles!`'s own
|
|
83
81
|
# suggestion, `Fuzzing::TargetCapabilities`).
|
|
82
|
+
#
|
|
83
|
+
# @param capability [String, Symbol] the capability name to look for
|
|
84
|
+
# @return [Array<String>] the names of every framework member that provides it,
|
|
85
|
+
# sorted
|
|
84
86
|
def self.providers_of(capability)
|
|
85
87
|
members.keys.select { |name| chapter(name).provides?(capability) }.sort
|
|
86
88
|
end
|
|
87
89
|
|
|
88
90
|
# One member's chapter, built in isolation.
|
|
91
|
+
#
|
|
92
|
+
# @param name [String, Symbol] the member's name, such as `"Governance"`
|
|
93
|
+
# @return [Bluebook::Chapter] the member's chapter, loaded into a scratch registry
|
|
94
|
+
# @raise [Runtime::WiringError] if no framework member has that name
|
|
89
95
|
def self.chapter(name)
|
|
90
96
|
path = members.fetch(name.to_s) do
|
|
91
97
|
raise Runtime::WiringError,
|
|
@@ -107,6 +113,23 @@ module Hecks
|
|
|
107
113
|
registry.bluebook(name.to_s)
|
|
108
114
|
end
|
|
109
115
|
|
|
116
|
+
# Idempotent, per registry — `uses_framework` is now called more than
|
|
117
|
+
# once for the same member within a single boot (S8: a domain
|
|
118
|
+
# attaching Governance for its own role check, plus a framework
|
|
119
|
+
# sibling attaching it too, e.g. `banking.hecksagon`'s Identity
|
|
120
|
+
# block). `Kernel.load` always re-executes the file, unlike
|
|
121
|
+
# `require`, so a second call would re-run `Hecks.bluebook
|
|
122
|
+
# "Governance"` a second time — and the self-hosting meta-domain
|
|
123
|
+
# records every declaration as a real dispatched command against its
|
|
124
|
+
# own ledger, so a second `Declare` for the same aggregate is a real
|
|
125
|
+
# `AlreadyExists`, not a no-op. Skipped once the member's bluebook is
|
|
126
|
+
# already registered in this registry — the same chapter, not merely
|
|
127
|
+
# a same-named one from a stale prior boot.
|
|
128
|
+
#
|
|
129
|
+
# @param name [String, Symbol] the member's name, such as `"Governance"`
|
|
130
|
+
# @return [Boolean, nil] true when this call loaded the member's bluebook, nil when
|
|
131
|
+
# the current registry already held it
|
|
132
|
+
# @raise [Runtime::WiringError] if no framework member has that name
|
|
110
133
|
def self.load!(name)
|
|
111
134
|
path = members.fetch(name.to_s) do
|
|
112
135
|
raise Runtime::WiringError,
|
data/lib/hecks/freezer.rb
CHANGED
|
@@ -8,7 +8,7 @@ module Hecks
|
|
|
8
8
|
# Hash a field holds stays mutable, so a caller reaches straight
|
|
9
9
|
# through and edits in place. Each fix looked complete and none was.
|
|
10
10
|
#
|
|
11
|
-
# What should be frozen, and why it is not everything
|
|
11
|
+
# ## What should be frozen, and why it is not everything
|
|
12
12
|
#
|
|
13
13
|
# a value object has no identity to change over. `with` already
|
|
14
14
|
# answers a new one rather than mutating, so freezing it through is
|
|
@@ -33,6 +33,10 @@ module Hecks
|
|
|
33
33
|
# Numbers, symbols, nil and booleans are already immediate or frozen;
|
|
34
34
|
# a Value froze itself when it was built. What is left is the mutable
|
|
35
35
|
# trio, and each has to be walked rather than topped.
|
|
36
|
+
#
|
|
37
|
+
# @param held [Object] any domain value
|
|
38
|
+
# @return [Object] `held`, recursively frozen if it is a Hash, Array, or
|
|
39
|
+
# String; returned unchanged otherwise
|
|
36
40
|
def deep(held)
|
|
37
41
|
case held
|
|
38
42
|
when Hash then held.each_value { |inner| deep(inner) }.freeze
|
|
@@ -45,11 +49,20 @@ module Hecks
|
|
|
45
49
|
# The question a gate asks, rather than the act. Answers false for the
|
|
46
50
|
# first thing that is reachable and mutable, which is what makes a
|
|
47
51
|
# failure message worth reading.
|
|
52
|
+
#
|
|
53
|
+
# @param held [Object] any domain value
|
|
54
|
+
# @return [Boolean] true if `held` and everything reachable from it is frozen
|
|
48
55
|
def deeply_frozen?(held) = unfrozen_within(held).nil?
|
|
49
56
|
|
|
50
57
|
# The path to the first mutable thing reachable from `held`, or nil.
|
|
51
58
|
# A path rather than a boolean because "something in this event is
|
|
52
59
|
# mutable" is not an actionable sentence.
|
|
60
|
+
#
|
|
61
|
+
# @param held [Object] any domain value
|
|
62
|
+
# @param path [Array<String>] the owner path accumulated by the recursive
|
|
63
|
+
# walk so far; callers pass nothing and get the default
|
|
64
|
+
# @return [String, nil] the dotted path (Hash keys, Array indices, or
|
|
65
|
+
# `"(the value itself)"`) to the first mutable value found, or nil if none
|
|
53
66
|
def unfrozen_within(held, path = [])
|
|
54
67
|
return (path.empty? ? "(the value itself)" : path.join(".")) unless immune?(held) || held.frozen?
|
|
55
68
|
|
|
@@ -62,6 +75,9 @@ module Hecks
|
|
|
62
75
|
# Immediates are frozen in every Ruby that matters, but asking
|
|
63
76
|
# `frozen?` of them and trusting the answer has bitten enough people
|
|
64
77
|
# that it is worth being explicit.
|
|
78
|
+
#
|
|
79
|
+
# @param held [Object] any domain value
|
|
80
|
+
# @return [Boolean] true if `held` is nil, true, false, a Numeric, or a Symbol
|
|
65
81
|
def immune?(held) = held.nil? || held == true || held == false || held.is_a?(Numeric) || held.is_a?(Symbol)
|
|
66
82
|
end
|
|
67
83
|
end
|