hecks 1.4.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 +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.hecksagon +19 -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/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 +58 -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/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/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 +29 -5
- data/lib/hecks/projector.rb +74 -6
- 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 +11 -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
|
@@ -38,13 +38,13 @@ module Hecks
|
|
|
38
38
|
# never closes it. Real domain activity is unaffected; the saga's
|
|
39
39
|
# own tracking of it is not. Left named rather than redesigning a
|
|
40
40
|
# corpus fixture that is not this checker's to redesign.
|
|
41
|
-
# S7, ADR 0025 —
|
|
42
|
-
# allowlist is gone, not just quieted: its "sent" state
|
|
43
|
-
# `state "x"` line never named by any handler's own from:/to:, a
|
|
41
|
+
# S7, ADR 0025 — an ExternalSettlement finding once lived here as an
|
|
42
|
+
# allowlist entry and is gone now, not just quieted: its "sent" state
|
|
43
|
+
# was a `state "x"` line never named by any handler's own from:/to:, a
|
|
44
44
|
# pure declaration-drift artifact. States are derived from the
|
|
45
45
|
# transitions that name them now (ProcessManagerBuilder#derived_
|
|
46
46
|
# states), so a state nothing ever transitions into or out of no
|
|
47
|
-
# longer exists to be unreachable — the finding
|
|
47
|
+
# longer exists to be unreachable — the finding that entry allowlisted
|
|
48
48
|
# cannot occur any more, by construction.
|
|
49
49
|
#
|
|
50
50
|
# "banking"/NotifyOnClosure, FlagKeyReturn — gone from here, moved
|
|
@@ -65,7 +65,7 @@ module Hecks
|
|
|
65
65
|
# in that domain's own declaration.
|
|
66
66
|
ALLOWED_FINDINGS = {
|
|
67
67
|
# QualityControl was the first domain in this corpus to trigger an
|
|
68
|
-
# `asks`/`tells` port operation from a `policy`, and
|
|
68
|
+
# `asks`/`tells` port operation from a `policy`, and once carried
|
|
69
69
|
# two entries here for it — both gone now, not just quieted:
|
|
70
70
|
#
|
|
71
71
|
# `deaf_policy` (ClearOnPass, RefuseOnFail, RecordTheIssue,
|
|
@@ -79,17 +79,17 @@ module Hecks
|
|
|
79
79
|
#
|
|
80
80
|
# `unknown_trigger` (FileWhenSubmitted, AskOnceMore) — BUG#23 — was
|
|
81
81
|
# never actually a `Naming`/`PolicyBuilder` defect, confirmed by
|
|
82
|
-
# tracing the real dispatch path rather than
|
|
83
|
-
# that
|
|
82
|
+
# tracing the real dispatch path rather than trusting a stale
|
|
83
|
+
# comment that once sat here: `Naming.command_ref`'s bare-constant
|
|
84
84
|
# rewrite does leave `trigger Ticket::IssueTracker::File` (aggregate,
|
|
85
85
|
# port, operation) as "Ticket::IssueTracker.File", a leftover `::`
|
|
86
86
|
# past the aggregate — but `PolicyInterpreter#deliver` re-qualifies
|
|
87
87
|
# every trigger with this domain's own name before dispatch
|
|
88
88
|
# ("QualityControl::Ticket::IssueTracker.File"), and `Naming.
|
|
89
89
|
# split_verb` already folds that reintroduced `::` into the
|
|
90
|
-
# dot-joined tail correctly (
|
|
91
|
-
# resolve_target
|
|
92
|
-
#
|
|
90
|
+
# dot-joined tail correctly (the same fix `ReactionInvocation#
|
|
91
|
+
# resolve_target` already carries) — confirmed live: a real
|
|
92
|
+
# dispatch through `Ticket.Submit` fires
|
|
93
93
|
# `IssueFiled`/`TicketFiled` exactly as declared. The actual gap was
|
|
94
94
|
# entirely in this checker: `verbs_of` never enumerated a port
|
|
95
95
|
# operation as a triggerable verb at all, and `policy_findings`
|
|
@@ -102,6 +102,9 @@ module Hecks
|
|
|
102
102
|
|
|
103
103
|
module_function
|
|
104
104
|
|
|
105
|
+
# Runs every static model check over `bluebook` and returns what it
|
|
106
|
+
# finds.
|
|
107
|
+
#
|
|
105
108
|
# `hecksagon:`/`known_domains:` — both optional, both `nil`-safe
|
|
106
109
|
# (every existing caller with no sibling hecksagon, or checking one
|
|
107
110
|
# domain in isolation, behaves exactly as before). `hecksagon` is
|
|
@@ -118,14 +121,34 @@ module Hecks
|
|
|
118
121
|
# `rust_target:`/`strict:` — both default false, both only change the
|
|
119
122
|
# severity of `rust_reserved_name` findings (see
|
|
120
123
|
# `rust_reserved_name_findings`); every other finding is unaffected.
|
|
121
|
-
|
|
124
|
+
#
|
|
125
|
+
# @param bluebook [Bluebook::Chapter] the assembled chapter to check
|
|
126
|
+
# @param hecksagon [Bluebook::Hecksagon, nil] this bluebook's own
|
|
127
|
+
# sibling wiring file, if the caller loaded one
|
|
128
|
+
# @param known_domains [Set<String>, nil] every bluebook/hecksagon
|
|
129
|
+
# name the caller has booted anywhere in this corpus scan
|
|
130
|
+
# @param global_emitted_events [Set<String>, nil] every bare event
|
|
131
|
+
# name any domain the caller has booted this run emits — lets a
|
|
132
|
+
# same-domain-target policy (e.g. one built by `translates`)
|
|
133
|
+
# reacting to a genuinely foreign event avoid a false `deaf_policy`
|
|
134
|
+
# finding; `nil` (every pre-existing call site) keeps today's
|
|
135
|
+
# single-domain-only check
|
|
136
|
+
# @param rust_target [Boolean] whether this domain has a real Rust
|
|
137
|
+
# target, raising `rust_reserved_name` findings to error
|
|
138
|
+
# @param strict [Boolean] whether to raise every `rust_reserved_name`
|
|
139
|
+
# finding to error regardless of `rust_target`
|
|
140
|
+
# @return [Array<Finding>] every finding this bluebook triggers,
|
|
141
|
+
# across its lifecycles, sagas, policies, and Rust-reserved names
|
|
142
|
+
def call(bluebook, hecksagon: nil, known_domains: nil, global_emitted_events: nil, rust_target: false, strict: false)
|
|
122
143
|
findings = []
|
|
123
144
|
bluebook.aggregates.each do |aggregate|
|
|
124
145
|
findings.concat(lifecycle_findings(aggregate, aggregate))
|
|
125
146
|
aggregate.entities.each { |entity| findings.concat(lifecycle_findings(aggregate, entity)) }
|
|
126
147
|
end
|
|
127
148
|
bluebook.process_managers.each { |process_manager| findings.concat(saga_findings(bluebook, process_manager)) }
|
|
128
|
-
bluebook.policies.each
|
|
149
|
+
bluebook.policies.each do |policy|
|
|
150
|
+
findings.concat(policy_findings(bluebook, policy, hecksagon, known_domains, global_emitted_events))
|
|
151
|
+
end
|
|
129
152
|
findings.concat(rust_reserved_name_findings(domain_name: bluebook.name,
|
|
130
153
|
aggregate_names: bluebook.aggregates.map(&:hecks_name),
|
|
131
154
|
rust_target: rust_target, strict: strict))
|
|
@@ -155,6 +178,18 @@ module Hecks
|
|
|
155
178
|
#
|
|
156
179
|
# The module-name transform is `downcase`, the one both generators
|
|
157
180
|
# apply to an aggregate name and to an attached chapter's name.
|
|
181
|
+
#
|
|
182
|
+
# @param domain_name [String, Symbol, nil] the domain's own name, or
|
|
183
|
+
# `nil` to skip the domain-level check
|
|
184
|
+
# @param aggregate_names [Array<String, Symbol>] every aggregate name
|
|
185
|
+
# to check
|
|
186
|
+
# @param rust_target [Boolean] whether this domain has a real Rust
|
|
187
|
+
# target, raising severity to error
|
|
188
|
+
# @param strict [Boolean] whether to raise severity to error
|
|
189
|
+
# regardless of `rust_target`
|
|
190
|
+
# @return [Array<Finding>] one `:rust_reserved_name` finding per
|
|
191
|
+
# aggregate or domain name that collides with a Rust or Cargo
|
|
192
|
+
# reserved word
|
|
158
193
|
def rust_reserved_name_findings(domain_name: nil, aggregate_names: [], rust_target: false, strict: false)
|
|
159
194
|
severity = rust_target || strict ? :error : :warning
|
|
160
195
|
keywords = Hecks::Vocabulary.fetch("RustReservedWord")
|
|
@@ -171,6 +206,17 @@ module Hecks
|
|
|
171
206
|
findings
|
|
172
207
|
end
|
|
173
208
|
|
|
209
|
+
# Checks the domain's own module/Cargo-feature name, one level up
|
|
210
|
+
# from `rust_reserved_name_findings`'s own per-aggregate check.
|
|
211
|
+
#
|
|
212
|
+
# @param domain_name [String, Symbol] the domain's own name
|
|
213
|
+
# @param keywords [Array<String>] the admitted `RustReservedWord`
|
|
214
|
+
# vocabulary rows
|
|
215
|
+
# @param severity [Symbol] the finding severity to use, `:error` or
|
|
216
|
+
# `:warning`
|
|
217
|
+
# @return [Array<Finding>] a single-element array holding a
|
|
218
|
+
# `:rust_reserved_name` finding, or `[]` if `domain_name` collides
|
|
219
|
+
# with no reserved word or Cargo key
|
|
174
220
|
def domain_reserved_name_findings(domain_name, keywords, severity)
|
|
175
221
|
module_name = rust_module_name(domain_name)
|
|
176
222
|
table = if keywords.include?(module_name)
|
|
@@ -185,10 +231,27 @@ module Hecks
|
|
|
185
231
|
"rename the domain")]
|
|
186
232
|
end
|
|
187
233
|
|
|
234
|
+
# Derives the Rust module identifier both generators derive from a
|
|
235
|
+
# name.
|
|
236
|
+
#
|
|
237
|
+
# @param name [String, Symbol] an aggregate or domain name
|
|
238
|
+
# @return [String] the Rust module identifier both generators derive
|
|
239
|
+
# from `name`
|
|
188
240
|
def rust_module_name(name) = name.to_s.downcase
|
|
189
241
|
|
|
190
242
|
# ── lifecycles (aggregate and entity — a piece may declare one too) ──
|
|
191
243
|
|
|
244
|
+
# Runs every lifecycle-shaped model check for one declaring
|
|
245
|
+
# construct's own lifecycle, if it has one.
|
|
246
|
+
#
|
|
247
|
+
# @param aggregate [Bluebook::Aggregate] the root aggregate `declaring`
|
|
248
|
+
# belongs to
|
|
249
|
+
# @param declaring [Bluebook::Aggregate, Bluebook::Entity] the
|
|
250
|
+
# construct whose own lifecycle to check — `aggregate` itself, or
|
|
251
|
+
# one of its entities
|
|
252
|
+
# @return [Array<Finding>] `[]` if `declaring` has no lifecycle;
|
|
253
|
+
# otherwise every `unknown_command`/`unreachable_state`/
|
|
254
|
+
# `dead_transition`/`stuck_state` finding its lifecycle triggers
|
|
192
255
|
def lifecycle_findings(aggregate, declaring)
|
|
193
256
|
lifecycle = declaring.lifecycle
|
|
194
257
|
return [] unless lifecycle
|
|
@@ -207,6 +270,15 @@ module Hecks
|
|
|
207
270
|
findings
|
|
208
271
|
end
|
|
209
272
|
|
|
273
|
+
# Finds every declared state a lifecycle's own reachability walk
|
|
274
|
+
# never reaches.
|
|
275
|
+
#
|
|
276
|
+
# @param lifecycle [Bluebook::Lifecycle] the lifecycle being checked
|
|
277
|
+
# @param full [Array<String>] every state `full_states` declares
|
|
278
|
+
# @param reached [Set<String>] every state `reachable_states` reaches
|
|
279
|
+
# @param subject [String] the finding's own subject label
|
|
280
|
+
# @return [Array<Finding>] one `:unreachable_state` finding per
|
|
281
|
+
# declared state that `reached` does not cover
|
|
210
282
|
def unreachable_state_findings(lifecycle, full, reached, subject)
|
|
211
283
|
(full - reached.to_a).map do |state|
|
|
212
284
|
Finding.new(kind: :unreachable_state, severity: :error, subject: subject,
|
|
@@ -215,6 +287,13 @@ module Hecks
|
|
|
215
287
|
end
|
|
216
288
|
end
|
|
217
289
|
|
|
290
|
+
# Finds every constrained transition that can never fire.
|
|
291
|
+
#
|
|
292
|
+
# @param lifecycle [Bluebook::Lifecycle] the lifecycle being checked
|
|
293
|
+
# @param reached [Set<String>] every state `reachable_states` reaches
|
|
294
|
+
# @param subject [String] the finding's own subject label
|
|
295
|
+
# @return [Array<Finding>] one `:dead_transition` finding per
|
|
296
|
+
# constrained transition whose `from:` states are all unreached
|
|
218
297
|
def dead_transition_findings(lifecycle, reached, subject)
|
|
219
298
|
lifecycle.transitions.filter_map do |command, transition|
|
|
220
299
|
next unless transition.constrained?
|
|
@@ -226,6 +305,14 @@ module Hecks
|
|
|
226
305
|
end
|
|
227
306
|
end
|
|
228
307
|
|
|
308
|
+
# Finds every reached state with no transition ever leaving it.
|
|
309
|
+
#
|
|
310
|
+
# @param lifecycle [Bluebook::Lifecycle] the lifecycle being checked
|
|
311
|
+
# @param reached [Set<String>] every state `reachable_states` reaches
|
|
312
|
+
# @param subject [String] the finding's own subject label
|
|
313
|
+
# @return [Array<Finding>] one `:stuck_state` finding per reached,
|
|
314
|
+
# non-exempt state with no transition ever leaving it (skipped
|
|
315
|
+
# entirely when any transition is unconstrained)
|
|
229
316
|
def stuck_state_findings(lifecycle, reached, subject)
|
|
230
317
|
any_unconstrained = lifecycle.transitions.any? { |_, t| !t.constrained? }
|
|
231
318
|
(reached - terminal_exempt(lifecycle)).filter_map do |state|
|
|
@@ -238,6 +325,15 @@ module Hecks
|
|
|
238
325
|
end
|
|
239
326
|
end
|
|
240
327
|
|
|
328
|
+
# Finds every transition named after a command this construct does
|
|
329
|
+
# not declare.
|
|
330
|
+
#
|
|
331
|
+
# @param lifecycle [Bluebook::Lifecycle] the lifecycle being checked
|
|
332
|
+
# @param commands [Array<String>] every command name the declaring
|
|
333
|
+
# construct actually declares
|
|
334
|
+
# @param subject [String] the finding's own subject label
|
|
335
|
+
# @return [Array<Finding>] one `:unknown_command` finding per
|
|
336
|
+
# transition named after a command `commands` does not list
|
|
241
337
|
def unknown_transition_commands(lifecycle, commands, subject)
|
|
242
338
|
lifecycle.transitions.filter_map do |command, _transition|
|
|
243
339
|
next if commands.include?(command)
|
|
@@ -251,6 +347,10 @@ module Hecks
|
|
|
251
347
|
# from-only state (declared nowhere as a target) is real and is
|
|
252
348
|
# exactly the hole `Lifecycle#states` leaves: it answers default
|
|
253
349
|
# plus targets only.
|
|
350
|
+
#
|
|
351
|
+
# @param lifecycle [Bluebook::Lifecycle] the lifecycle being checked
|
|
352
|
+
# @return [Array<String>] every state `lifecycle` declares, in any
|
|
353
|
+
# role (default, target, or from), unique
|
|
254
354
|
def full_states(lifecycle)
|
|
255
355
|
(
|
|
256
356
|
[lifecycle.default] +
|
|
@@ -262,6 +362,10 @@ module Hecks
|
|
|
262
362
|
# Least fixpoint from the default state: an unconstrained
|
|
263
363
|
# transition always fires, from wherever the machine is ; a
|
|
264
364
|
# constrained one fires once any of its named sources is reached.
|
|
365
|
+
#
|
|
366
|
+
# @param lifecycle [Bluebook::Lifecycle] the lifecycle being checked
|
|
367
|
+
# @return [Set<String>] every state reachable from `lifecycle`'s own
|
|
368
|
+
# default state
|
|
265
369
|
def reachable_states(lifecycle)
|
|
266
370
|
reached = Set.new([lifecycle.default])
|
|
267
371
|
loop do
|
|
@@ -299,6 +403,10 @@ module Hecks
|
|
|
299
403
|
# should still warn (see spec/fixtures/model_check/lifecycle_
|
|
300
404
|
# findings.bluebook's own Widget::Part, which stays warned on
|
|
301
405
|
# purpose).
|
|
406
|
+
#
|
|
407
|
+
# @param lifecycle [Bluebook::Lifecycle] the lifecycle being checked
|
|
408
|
+
# @return [Array<String>] `[lifecycle.default]` when it is exempt from
|
|
409
|
+
# the stuck-state warning, `[]` otherwise
|
|
302
410
|
def terminal_exempt(lifecycle)
|
|
303
411
|
return [] if lifecycle.transitions.empty?
|
|
304
412
|
|
|
@@ -308,6 +416,15 @@ module Hecks
|
|
|
308
416
|
|
|
309
417
|
# ── process managers / sagas ──────────────────────────────────────
|
|
310
418
|
|
|
419
|
+
# Runs every saga-shaped model check for one process manager.
|
|
420
|
+
#
|
|
421
|
+
# @param bluebook [Bluebook::Chapter] the assembled chapter
|
|
422
|
+
# `process_manager` belongs to
|
|
423
|
+
# @param process_manager [Bluebook::ProcessManager] the process
|
|
424
|
+
# manager to check
|
|
425
|
+
# @return [Array<Finding>] every `deaf_trigger`/`unreachable_pm_state`/
|
|
426
|
+
# `deaf_handler`/`unknown_dispatch`/`unarmed_compensation`/
|
|
427
|
+
# `dead_compensation` finding this process manager triggers
|
|
311
428
|
def saga_findings(bluebook, process_manager)
|
|
312
429
|
emitted = emitted_events(bluebook)
|
|
313
430
|
# (domain, aggregate, command) triples, not raw strings — see
|
|
@@ -328,6 +445,15 @@ module Hecks
|
|
|
328
445
|
findings
|
|
329
446
|
end
|
|
330
447
|
|
|
448
|
+
# Finds every declared `starts_on`/`ends_on` event this domain never
|
|
449
|
+
# emits.
|
|
450
|
+
#
|
|
451
|
+
# @param process_manager [Bluebook::ProcessManager] the process
|
|
452
|
+
# manager being checked
|
|
453
|
+
# @param emitted [Array<String>] every bare event name this domain
|
|
454
|
+
# actually emits
|
|
455
|
+
# @return [Array<Finding>] one `:deaf_trigger` finding per declared
|
|
456
|
+
# `starts_on`/`ends_on` event this domain never emits
|
|
331
457
|
def deaf_trigger_findings(process_manager, emitted)
|
|
332
458
|
[process_manager.starts_on, process_manager.ends_on].compact.filter_map do |event|
|
|
333
459
|
next if emitted.include?(bare(event))
|
|
@@ -338,6 +464,14 @@ module Hecks
|
|
|
338
464
|
end
|
|
339
465
|
end
|
|
340
466
|
|
|
467
|
+
# Finds every declared state no handler chain ever reaches.
|
|
468
|
+
#
|
|
469
|
+
# @param process_manager [Bluebook::ProcessManager] the process
|
|
470
|
+
# manager being checked
|
|
471
|
+
# @param reached [Set<String>] every state `pm_reachable_states`
|
|
472
|
+
# reaches
|
|
473
|
+
# @return [Array<Finding>] one `:unreachable_pm_state` finding per
|
|
474
|
+
# declared state that `reached` does not cover
|
|
341
475
|
def unreachable_pm_state_findings(process_manager, reached)
|
|
342
476
|
(Array(process_manager.states) - reached.to_a).map do |state|
|
|
343
477
|
Finding.new(kind: :unreachable_pm_state, severity: :error, subject: process_manager.name,
|
|
@@ -352,11 +486,25 @@ module Hecks
|
|
|
352
486
|
# check reads only this handler plus the domain-wide emitted/verbs
|
|
353
487
|
# sets saga_findings already resolved once, no state shared between
|
|
354
488
|
# handlers.
|
|
489
|
+
#
|
|
490
|
+
# @param bluebook [Bluebook::Chapter] the assembled chapter
|
|
491
|
+
# `process_manager` belongs to
|
|
492
|
+
# @param process_manager [Bluebook::ProcessManager] the process
|
|
493
|
+
# manager `handler` belongs to
|
|
494
|
+
# @param emitted [Array<String>] every bare event name this domain
|
|
495
|
+
# actually emits
|
|
496
|
+
# @param verbs [Array<Array(String, String, String)>] every command
|
|
497
|
+
# this domain declares, parsed as `Naming.split_verb`
|
|
498
|
+
# `[domain, aggregate, command]` triples
|
|
499
|
+
# @param handler [Bluebook::ProcessManagerHandler] the handler to
|
|
500
|
+
# check
|
|
501
|
+
# @return [Array<Finding>] every `deaf_handler`/`unknown_dispatch`/
|
|
502
|
+
# `unarmed_compensation` finding this handler triggers
|
|
355
503
|
def handler_findings(bluebook, process_manager, emitted, verbs, handler)
|
|
356
504
|
findings = []
|
|
357
505
|
|
|
358
|
-
# The compensating leg answers REFUSED
|
|
359
|
-
# command ever emits by name (ProcessManager::REFUSED) — not
|
|
506
|
+
# The compensating leg answers `REFUSED`, a synthetic trigger no
|
|
507
|
+
# command ever emits by name (`ProcessManager::REFUSED`) — not
|
|
360
508
|
# a deaf handler, the one handler this domain's own events can
|
|
361
509
|
# never satisfy on purpose.
|
|
362
510
|
if handler.event_type != ProcessManager::REFUSED && !emitted.include?(bare(handler.event_type))
|
|
@@ -367,16 +515,16 @@ module Hecks
|
|
|
367
515
|
|
|
368
516
|
handler.dispatches.each do |dispatch|
|
|
369
517
|
# **Always this domain** — same fix, same reason, as `SagaInterpreter
|
|
370
|
-
# #qualified` (BUG#6).
|
|
371
|
-
# `command_name` still
|
|
372
|
-
# command_ref`'s own rewrite
|
|
373
|
-
#
|
|
518
|
+
# #qualified` (BUG#6). Guessing instead — reading a dispatch whose
|
|
519
|
+
# own `command_name` still carries a leftover `::` after `Naming.
|
|
520
|
+
# command_ref`'s own rewrite as "already qualified" and leaving it
|
|
521
|
+
# alone — would hit the exact same string-shape ambiguity that
|
|
374
522
|
# `SagaInterpreter#qualified`'s own comment explains at length
|
|
375
523
|
# (a same-domain entity command reference and a genuinely
|
|
376
524
|
# cross-domain one are textually indistinguishable after that
|
|
377
525
|
# rewrite). Confirmed against the entire corpus, same as that
|
|
378
526
|
# fix: no saga anywhere ever dispatches genuinely cross-domain,
|
|
379
|
-
# so this checker
|
|
527
|
+
# so this checker qualifies exactly the way the runtime
|
|
380
528
|
# actually dispatches — unconditionally against `bluebook.name`
|
|
381
529
|
# — instead of maintaining its own, independently-wrong copy of
|
|
382
530
|
# the same guess.
|
|
@@ -410,7 +558,7 @@ module Hecks
|
|
|
410
558
|
# shape of the real bug this whole feature closes ("the
|
|
411
559
|
# reversal was written and never armed"), caught at build/
|
|
412
560
|
# model-check time instead of discovered in production. No
|
|
413
|
-
# handler anywhere answers REFUSED (`process_manager.saga?` false) means
|
|
561
|
+
# handler anywhere answers `REFUSED` (`process_manager.saga?` false) means
|
|
414
562
|
# `SagaInterpreter#unwind` never runs for this process
|
|
415
563
|
# manager at all, so a declared `compensates` is structurally
|
|
416
564
|
# unreachable — not a warning about style, a dead declaration.
|
|
@@ -426,6 +574,16 @@ module Hecks
|
|
|
426
574
|
findings
|
|
427
575
|
end
|
|
428
576
|
|
|
577
|
+
# Checks whether a saga's own compensation leaves an unreachable
|
|
578
|
+
# state.
|
|
579
|
+
#
|
|
580
|
+
# @param process_manager [Bluebook::ProcessManager] the process
|
|
581
|
+
# manager being checked
|
|
582
|
+
# @param reached [Set<String>] every state `pm_reachable_states`
|
|
583
|
+
# reaches
|
|
584
|
+
# @return [Array<Finding>] a single-element array holding a
|
|
585
|
+
# `:dead_compensation` finding, or `[]` if `process_manager` has no
|
|
586
|
+
# saga or its own compensation leaves a reached state
|
|
429
587
|
def dead_compensation_findings(process_manager, reached)
|
|
430
588
|
return [] unless process_manager.saga? && !reached.include?(process_manager.saga.from_state)
|
|
431
589
|
|
|
@@ -435,13 +593,21 @@ module Hecks
|
|
|
435
593
|
end
|
|
436
594
|
|
|
437
595
|
# A handler edge is only usable in the closure if it can actually
|
|
438
|
-
# fire — REFUSED always can (it is a compensation trigger, not an
|
|
596
|
+
# fire — `REFUSED` always can (it is a compensation trigger, not an
|
|
439
597
|
# event), and any other handler needs its event genuinely emitted.
|
|
440
598
|
# Without this, a deaf handler's declared from_state -> to_state
|
|
441
599
|
# pair reads as connected even though nothing can ever traverse
|
|
442
600
|
# it, which would hide exactly the states this walk exists to
|
|
443
601
|
# catch (a state only "reachable" through a handler that itself
|
|
444
602
|
# never fires).
|
|
603
|
+
#
|
|
604
|
+
# @param process_manager [Bluebook::ProcessManager] the process
|
|
605
|
+
# manager being checked
|
|
606
|
+
# @param emitted [Array<String>] every bare event name this domain
|
|
607
|
+
# actually emits
|
|
608
|
+
# @return [Set<String>] every state reachable from
|
|
609
|
+
# `process_manager`'s own first declared state, through handlers
|
|
610
|
+
# that can actually fire
|
|
445
611
|
def pm_reachable_states(process_manager, emitted)
|
|
446
612
|
return Set.new if Array(process_manager.states).empty?
|
|
447
613
|
|
|
@@ -463,7 +629,21 @@ module Hecks
|
|
|
463
629
|
|
|
464
630
|
# ── policies ───────────────────────────────────────────────────────
|
|
465
631
|
|
|
466
|
-
|
|
632
|
+
# Runs every same-domain policy check, or defers to
|
|
633
|
+
# `cross_domain_policy_findings` for a policy that targets another
|
|
634
|
+
# domain.
|
|
635
|
+
#
|
|
636
|
+
# @param bluebook [Bluebook::Chapter] the assembled chapter `policy`
|
|
637
|
+
# belongs to
|
|
638
|
+
# @param policy [Bluebook::Policy] the policy to check
|
|
639
|
+
# @param hecksagon [Bluebook::Hecksagon, nil] this bluebook's own
|
|
640
|
+
# sibling wiring file, if the caller loaded one
|
|
641
|
+
# @param known_domains [Set<String>, nil] every bluebook/hecksagon
|
|
642
|
+
# name the caller has booted anywhere in this corpus scan
|
|
643
|
+
# @return [Array<Finding>] every `deaf_policy`/`unknown_trigger`
|
|
644
|
+
# finding this policy triggers, or `cross_domain_policy_findings`'s
|
|
645
|
+
# own return for a cross-domain policy
|
|
646
|
+
def policy_findings(bluebook, policy, hecksagon, known_domains, global_emitted_events = nil)
|
|
467
647
|
return cross_domain_policy_findings(policy, hecksagon, known_domains) if policy.target_domain
|
|
468
648
|
|
|
469
649
|
emitted = emitted_events(bluebook)
|
|
@@ -480,7 +660,18 @@ module Hecks
|
|
|
480
660
|
# prior aggregate-scoped policy (ReviewOnFreeze) is also cross-domain
|
|
481
661
|
# (`across "Compliance"`), which exits this method one line above
|
|
482
662
|
# before the mismatch is ever reached.
|
|
483
|
-
|
|
663
|
+
# `global_emitted_events` — a `translates` (hecksagon-level)
|
|
664
|
+
# reaction is BUILT as the exact same same-domain-target `Policy`
|
|
665
|
+
# an ordinary `policy` block is (no `target_domain`, so it never
|
|
666
|
+
# reaches `cross_domain_policy_findings` above), but its whole
|
|
667
|
+
# point is to react to a FOREIGN domain's own event — this
|
|
668
|
+
# domain's own `emitted_events(bluebook)` was never going to
|
|
669
|
+
# contain it. Checked only as a FALLBACK, after the local check
|
|
670
|
+
# already failed, so a real same-domain typo still gets flagged
|
|
671
|
+
# exactly as before whenever the caller has no cross-domain set
|
|
672
|
+
# to offer (nil — every existing call site, unchanged) or the
|
|
673
|
+
# event genuinely isn't emitted anywhere this run examined.
|
|
674
|
+
unless emitted.include?(policy.event_name) || global_emitted_events&.include?(policy.event_name)
|
|
484
675
|
findings << Finding.new(kind: :deaf_policy, severity: :error, subject: policy.name,
|
|
485
676
|
message: "on #{policy.on_event.inspect}, which no command in this domain emits")
|
|
486
677
|
end
|
|
@@ -536,9 +727,20 @@ module Hecks
|
|
|
536
727
|
# DDD vocabulary (Shared Kernel, Customer/Supplier) lives here, in
|
|
537
728
|
# the finding's own name and this comment, and in prose docs — not
|
|
538
729
|
# in the grammar.
|
|
730
|
+
#
|
|
731
|
+
# @param policy [Bluebook::Policy] the cross-domain policy to check
|
|
732
|
+
# @param hecksagon [Bluebook::Hecksagon, nil] this bluebook's own
|
|
733
|
+
# sibling wiring file, if the caller loaded one
|
|
734
|
+
# @param known_domains [Set<String>, nil] every bluebook/hecksagon
|
|
735
|
+
# name the caller has booted anywhere in this corpus scan
|
|
736
|
+
# @return [Array<Finding>] `expected_undelivered_findings`'s own
|
|
737
|
+
# return for a policy declared `expect_undelivered`; otherwise every
|
|
738
|
+
# `contradictory_relationship`/`unacknowledged_relationship`/
|
|
739
|
+
# `unknown_target_domain` finding this policy triggers
|
|
539
740
|
def cross_domain_policy_findings(policy, hecksagon, known_domains)
|
|
540
741
|
return expected_undelivered_findings(policy, hecksagon, known_domains) if policy.expect_undelivered
|
|
541
|
-
|
|
742
|
+
# No sibling hecksagon loaded — nothing to check a relationship against.
|
|
743
|
+
return [] unless hecksagon
|
|
542
744
|
|
|
543
745
|
target = policy.target_domain
|
|
544
746
|
findings = []
|
|
@@ -601,6 +803,16 @@ module Hecks
|
|
|
601
803
|
# the reaction can be delivered, and the declaration is now a lie.
|
|
602
804
|
# `known_domains` is nil for a single-target run, which can then only
|
|
603
805
|
# check the hecksagon half.
|
|
806
|
+
#
|
|
807
|
+
# @param policy [Bluebook::Policy] the `expect_undelivered` policy to
|
|
808
|
+
# check
|
|
809
|
+
# @param hecksagon [Bluebook::Hecksagon, nil] this bluebook's own
|
|
810
|
+
# sibling wiring file, if the caller loaded one
|
|
811
|
+
# @param known_domains [Set<String>, nil] every bluebook/hecksagon
|
|
812
|
+
# name the caller has booted anywhere in this corpus scan
|
|
813
|
+
# @return [Array<Finding>] a single-element array holding a
|
|
814
|
+
# `:stale_undelivered_expectation` finding, or `[]` if the target
|
|
815
|
+
# remains genuinely unreachable
|
|
604
816
|
def expected_undelivered_findings(policy, hecksagon, known_domains)
|
|
605
817
|
target = policy.target_domain
|
|
606
818
|
reached = []
|
|
@@ -641,6 +853,11 @@ module Hecks
|
|
|
641
853
|
# read both. `.compact` because an inbound operation's `.answers`/
|
|
642
854
|
# `.refuses` are always nil (there is no channel back to tell), which
|
|
643
855
|
# would otherwise seed every emitted-events set with a stray nil.
|
|
856
|
+
#
|
|
857
|
+
# @param bluebook [Bluebook::Chapter] the assembled chapter to
|
|
858
|
+
# enumerate
|
|
859
|
+
# @return [Array<String>] every bare event name this domain's
|
|
860
|
+
# commands and port operations emit, answer, or refuse, unique
|
|
644
861
|
def emitted_events(bluebook)
|
|
645
862
|
aggregate_emits = bluebook.aggregates.flat_map do |aggregate|
|
|
646
863
|
aggregate.commands.map(&:emits) +
|
|
@@ -659,6 +876,11 @@ module Hecks
|
|
|
659
876
|
# instead. Pulled out of `emitted_events` above purely to keep that
|
|
660
877
|
# method's own branching low enough to read at a glance — every
|
|
661
878
|
# port, aggregate-owned or chapter-level, asks this the same way.
|
|
879
|
+
#
|
|
880
|
+
# @param ports [Array<Bluebook::DomainPort>] the ports to enumerate
|
|
881
|
+
# @return [Array<String, nil>] every event name each operation emits,
|
|
882
|
+
# answers, or refuses; `nil` entries included (compacted by the
|
|
883
|
+
# caller)
|
|
662
884
|
def port_operation_events(ports)
|
|
663
885
|
ports.flat_map { |port| port.operations.flat_map { |op| [*op.emits, op.answers, op.refuses] } }
|
|
664
886
|
end
|
|
@@ -667,6 +889,12 @@ module Hecks
|
|
|
667
889
|
# carries and fuzzing/sequence_generator/catalog.rb builds
|
|
668
890
|
# independently for the same reason: a saga dispatch and a fuzzer
|
|
669
891
|
# step both have to name a verb the same way the door does.
|
|
892
|
+
#
|
|
893
|
+
# @param bluebook [Bluebook::Chapter] the assembled chapter to
|
|
894
|
+
# enumerate
|
|
895
|
+
# @return [Array<String>] every command's own fully-qualified verb,
|
|
896
|
+
# `"Domain::Aggregate.Command"` or
|
|
897
|
+
# `"Domain::Aggregate.Entity.Command"`
|
|
670
898
|
def verbs_of(bluebook)
|
|
671
899
|
bluebook.aggregates.flat_map do |aggregate|
|
|
672
900
|
verbs = aggregate.commands.map { |command| "#{bluebook.name}::#{aggregate.hecks_name}.#{command.hecks_name}" }
|
|
@@ -687,6 +915,11 @@ module Hecks
|
|
|
687
915
|
# Only an aggregate's own ports (`aggregate.ports`) are in scope here
|
|
688
916
|
# — a policy's `trigger` always names one aggregate, never a chapter-
|
|
689
917
|
# level port with no owner to address through.
|
|
918
|
+
#
|
|
919
|
+
# @param bluebook [Bluebook::Chapter] the assembled chapter to
|
|
920
|
+
# enumerate
|
|
921
|
+
# @return [Array<String>] every aggregate-owned port operation's own
|
|
922
|
+
# fully-qualified verb, `"Domain::Aggregate.Port.Operation"`
|
|
690
923
|
def port_verbs_of(bluebook)
|
|
691
924
|
bluebook.aggregates.flat_map do |aggregate|
|
|
692
925
|
aggregate.ports.flat_map do |port|
|
|
@@ -703,10 +936,21 @@ module Hecks
|
|
|
703
936
|
# spellings of the same verb as strings (see `policy_findings`'s own
|
|
704
937
|
# `unknown_trigger` check for why that comparison has to happen this
|
|
705
938
|
# way, not as `include?` on a raw string).
|
|
939
|
+
#
|
|
940
|
+
# @param bluebook [Bluebook::Chapter] the assembled chapter to
|
|
941
|
+
# enumerate
|
|
942
|
+
# @return [Set<Array(String, String, String)>] every triggerable
|
|
943
|
+
# verb, parsed as `Naming.split_verb` `[domain, aggregate,
|
|
944
|
+
# command]` triples
|
|
706
945
|
def triggerable_verbs(bluebook)
|
|
707
946
|
(verbs_of(bluebook) + port_verbs_of(bluebook)).to_set { |verb| Naming.split_verb(verb) }
|
|
708
947
|
end
|
|
709
948
|
|
|
949
|
+
# Strips a domain qualifier off an event name.
|
|
950
|
+
#
|
|
951
|
+
# @param event [String, Symbol] an event name, qualified
|
|
952
|
+
# (`"Domain::Event"`) or bare
|
|
953
|
+
# @return [String] `event`'s own bare name, past the last `::`
|
|
710
954
|
def bare(event) = event.to_s.split("::").last
|
|
711
955
|
end
|
|
712
956
|
end
|
|
@@ -72,6 +72,10 @@ module Hecks
|
|
|
72
72
|
# methods agreeing about a shared cursor" — exactly what the walk's
|
|
73
73
|
# own comment says a cleverer spelling would obscure.
|
|
74
74
|
# rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
75
|
+
#
|
|
76
|
+
# @param pattern [String, Symbol, #to_s] the declared `pattern:` regex source
|
|
77
|
+
# @return [Rejection, nil] the reason the pattern is refused, or `nil` if it is
|
|
78
|
+
# entirely within the portable subset
|
|
75
79
|
def validate(pattern)
|
|
76
80
|
chars = pattern.to_s.chars
|
|
77
81
|
index = 0
|
|
@@ -145,8 +149,17 @@ module Hecks
|
|
|
145
149
|
possessive: "possessive quantifier"
|
|
146
150
|
}.freeze
|
|
147
151
|
|
|
152
|
+
# Builds the rejection for one refused construct.
|
|
153
|
+
#
|
|
154
|
+
# @param key [Symbol] a key of `CONSTRUCTS`/`REASONS`, such as `:lookahead`
|
|
155
|
+
# @return [Rejection] the construct's name and the reason it is refused
|
|
148
156
|
def refuse(key) = Rejection.new(CONSTRUCTS.fetch(key), REASONS.fetch(key))
|
|
149
157
|
|
|
158
|
+
# Says whether a POSIX bracket class (`[:digit:]` and friends) starts at `index`.
|
|
159
|
+
#
|
|
160
|
+
# @param chars [Array<String>] the pattern, split into characters
|
|
161
|
+
# @param index [Integer] the position to check
|
|
162
|
+
# @return [Boolean] whether a POSIX bracket class starts at `index`
|
|
150
163
|
def posix_class_at?(chars, index)
|
|
151
164
|
return false unless chars[index] == "[" && chars[index + 1] == ":"
|
|
152
165
|
|
|
@@ -156,9 +169,13 @@ module Hecks
|
|
|
156
169
|
end
|
|
157
170
|
|
|
158
171
|
# A possessive quantifier is `*+`, `++`, `?+`, or a bounded `{n}`/{n,m}`
|
|
159
|
-
# immediately followed by `+` — only checked
|
|
172
|
+
# immediately followed by `+` — only checked outside a character class,
|
|
160
173
|
# where `*`, `+`, `?`, `{`, `}` are quantifier syntax rather than
|
|
161
174
|
# literal characters.
|
|
175
|
+
#
|
|
176
|
+
# @param chars [Array<String>] the pattern, split into characters
|
|
177
|
+
# @param index [Integer] the position to check
|
|
178
|
+
# @return [Boolean] whether a possessive quantifier starts at `index`
|
|
162
179
|
def possessive_at?(chars, index)
|
|
163
180
|
return true if %w[* + ?].include?(chars[index]) && chars[index + 1] == "+"
|
|
164
181
|
return false unless chars[index] == "{"
|
|
@@ -169,6 +186,11 @@ module Hecks
|
|
|
169
186
|
|
|
170
187
|
# Length of a `{n}` / `{n,}` / `{n,m}` bound starting at `index`, or nil
|
|
171
188
|
# if what's there isn't one.
|
|
189
|
+
#
|
|
190
|
+
# @param chars [Array<String>] the pattern, split into characters
|
|
191
|
+
# @param index [Integer] the position the bound is expected to start at
|
|
192
|
+
# @return [Integer, nil] the bound's length in characters, or `nil` if `index`
|
|
193
|
+
# does not start a `{n}`/`{n,}`/`{n,m}` bound
|
|
172
194
|
def bounded_quantifier_length(chars, index)
|
|
173
195
|
cursor = index + 1
|
|
174
196
|
digit_seen = false
|
|
@@ -69,6 +69,10 @@ module Hecks
|
|
|
69
69
|
# per-dispatch saga compensation instead — a deliberate choice, not
|
|
70
70
|
# an accidental collision.
|
|
71
71
|
Saga = Struct.new(:trigger, :from_state, :to_state, :compensations, keyword_init: true) do
|
|
72
|
+
# Lists the commands this saga sends to undo a refused leg.
|
|
73
|
+
#
|
|
74
|
+
# @return [Array<String>] the name of each compensating command, in the order
|
|
75
|
+
# `compensations` lists them
|
|
72
76
|
def undoes = compensations.map(&:command_name)
|
|
73
77
|
|
|
74
78
|
def to_s = "#{trigger} → #{to_state} (#{undoes.join(', ')})"
|
|
@@ -120,6 +124,15 @@ module Hecks
|
|
|
120
124
|
|
|
121
125
|
attr_reader :name, :correlates_by, :starts_on, :ends_on, :states, :handlers
|
|
122
126
|
|
|
127
|
+
# @param name [String, Symbol] the process manager's declared name
|
|
128
|
+
# @param correlates_by [Symbol, nil] the payload field a triggering event's
|
|
129
|
+
# instances are correlated by
|
|
130
|
+
# @param starts_on [String, nil] the event that starts a new instance
|
|
131
|
+
# @param ends_on [String, nil] the event that ends an instance
|
|
132
|
+
# @param states [Array<String>] the declared states this procedure's instances pass
|
|
133
|
+
# through
|
|
134
|
+
# @param handlers [Array<Bluebook::ProcessManagerHandler>] the declared handler rows,
|
|
135
|
+
# one per (event, state) leg
|
|
123
136
|
def initialize(name:, correlates_by: nil, starts_on: nil, ends_on: nil,
|
|
124
137
|
states: [], handlers: [])
|
|
125
138
|
@name = name.to_s
|
|
@@ -8,10 +8,15 @@ module Hecks
|
|
|
8
8
|
|
|
9
9
|
attr_reader :root
|
|
10
10
|
|
|
11
|
+
# @param root [String] the directory to search under
|
|
11
12
|
def initialize(root)
|
|
12
13
|
@root = File.expand_path(root)
|
|
13
14
|
end
|
|
14
15
|
|
|
16
|
+
# Walks `root` for every directory that holds `.bluebook` files.
|
|
17
|
+
#
|
|
18
|
+
# @return [Array<String>] the absolute path of each `bluebook` directory found,
|
|
19
|
+
# sorted; a directory named in `SKIPPED_DIRECTORIES` is pruned from the walk
|
|
15
20
|
def bluebook_directories
|
|
16
21
|
found = []
|
|
17
22
|
Find.find(root) do |path|
|