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
data/lib/hecks/doc/reference.rb
CHANGED
|
@@ -19,7 +19,7 @@ module Hecks
|
|
|
19
19
|
GENERATED_END = "<!-- generated:end -->".freeze
|
|
20
20
|
TODO_SENTINEL = "<!-- TODO: document this word -->".freeze
|
|
21
21
|
|
|
22
|
-
# A
|
|
22
|
+
# A page's own hand-written opening, harvested under a key no word
|
|
23
23
|
# can ever collide with (words are strings off the Syntax chapter;
|
|
24
24
|
# this is a Symbol). It exists so a page can boot once — load a real
|
|
25
25
|
# corpus chapter, wire its hexagon — and have every word's example
|
|
@@ -31,18 +31,36 @@ module Hecks
|
|
|
31
31
|
|
|
32
32
|
module_function
|
|
33
33
|
|
|
34
|
+
# The marker opening one word's generated region.
|
|
35
|
+
#
|
|
36
|
+
# @param word [String, Symbol, #to_s] the word this section documents
|
|
37
|
+
# @return [String] the HTML comment marking that word's generated region open
|
|
34
38
|
def generated_begin(word) = "<!-- generated:begin word=#{word} -->"
|
|
35
39
|
|
|
36
40
|
# Keyed by region rather than by word — the same marker convention,
|
|
37
41
|
# used for the parts of a page that are not about one word: a
|
|
38
42
|
# page's generated lede here, README's generated indexes below.
|
|
43
|
+
#
|
|
44
|
+
# @param id [String, Symbol, #to_s] the region's id, such as `"page"` or `"tools"`
|
|
45
|
+
# @return [String] the HTML comment marking that region's generated region open
|
|
39
46
|
def region_begin(id) = "<!-- generated:begin id=#{id} -->"
|
|
40
47
|
|
|
48
|
+
# The language's own Syntax aggregate, read off the judged grammar chapter.
|
|
49
|
+
#
|
|
50
|
+
# @return [Bluebook::Aggregate, nil] the Syntax aggregate, or nil if the
|
|
51
|
+
# grammar's Bluebook chapter declares none by that name
|
|
41
52
|
def syntax
|
|
42
53
|
meta = Bluebook::MetaValidator.grammar_registry.bluebook("Bluebook")
|
|
43
54
|
meta.aggregates.find { |aggregate| aggregate.hecks_name == "Syntax" }
|
|
44
55
|
end
|
|
45
56
|
|
|
57
|
+
# Reads one closed-set value object's declared members off the Syntax
|
|
58
|
+
# aggregate, as string-valued Hashes.
|
|
59
|
+
#
|
|
60
|
+
# @param name [String, Symbol, #to_s] the value object's `hecks_name`,
|
|
61
|
+
# such as `"Keyword"`
|
|
62
|
+
# @return [Array<Hash{Symbol => String}>] each declared member's fields,
|
|
63
|
+
# values stringified
|
|
46
64
|
def rows(name)
|
|
47
65
|
syntax.value_objects.find { |vo| vo.hecks_name == name }
|
|
48
66
|
.members.map { |row| row.to_h.transform_values(&:to_s) }
|
|
@@ -53,31 +71,60 @@ module Hecks
|
|
|
53
71
|
# really is a lifecycle. `SyntaxBoot.call` discovers the static
|
|
54
72
|
# aggregate-local seed rows (`KeywordSeed`/`ArgumentSeed`), dispatches each
|
|
55
73
|
# one through the real admission/lifecycle door, and hands back the
|
|
56
|
-
# same shape `rows
|
|
57
|
-
# change.
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
#
|
|
67
|
-
#
|
|
74
|
+
# same shape `rows`, above, already produces — nothing below this
|
|
75
|
+
# needed to change.
|
|
76
|
+
#
|
|
77
|
+
# This module holds no separate `@keywords ||=` memo of its own —
|
|
78
|
+
# delegates straight through to `SyntaxBoot.call`'s own cache
|
|
79
|
+
# (keyed on the grammar registry's own chapter set — see its
|
|
80
|
+
# comment) rather than shadowing it. A second, local cache on top
|
|
81
|
+
# of that one would double-cache with no way to invalidate either
|
|
82
|
+
# half: whichever call happened to land first would lock in
|
|
83
|
+
# forever, even one caught mid-build missing every Paging-attached
|
|
84
|
+
# word (limit/offset/cursor/nulls).
|
|
85
|
+
#
|
|
86
|
+
# @return [Array<Hash{Symbol => String}>] every declared Keyword row
|
|
68
87
|
def keywords = Bluebook::MetaValidator::SyntaxBoot.call[:keywords]
|
|
88
|
+
|
|
89
|
+
# Every declared Argument row.
|
|
90
|
+
#
|
|
91
|
+
# @return [Array<Hash{Symbol => String}>] every declared Argument row
|
|
69
92
|
def arguments = Bluebook::MetaValidator::SyntaxBoot.call[:arguments]
|
|
70
93
|
|
|
94
|
+
# Reads a row's declared status, defaulting when it declared none.
|
|
95
|
+
#
|
|
96
|
+
# @param row [Hash{Symbol => String}] a Keyword or Argument row
|
|
97
|
+
# @return [String] the row's declared status, defaulting to `"admitted"`
|
|
98
|
+
# when it declared none
|
|
71
99
|
def status_of(row) = row[:status].to_s.empty? ? "admitted" : row[:status].to_s
|
|
100
|
+
|
|
101
|
+
# Whether a row is still current enough to appear in the reference.
|
|
102
|
+
#
|
|
103
|
+
# @param row [Hash{Symbol => String}] a Keyword or Argument row
|
|
104
|
+
# @return [Boolean] true if the row's status is `"admitted"` or `"deprecated"`
|
|
72
105
|
def live?(row) = %w[admitted deprecated].include?(status_of(row))
|
|
73
106
|
|
|
107
|
+
# Every distinct context a keyword is declared in.
|
|
108
|
+
#
|
|
109
|
+
# @return [Array<String>] every distinct context a keyword is declared in
|
|
74
110
|
def contexts = keywords.map { |row| row[:context] }.uniq
|
|
75
111
|
|
|
112
|
+
# Derives a context's reference page filename.
|
|
113
|
+
#
|
|
114
|
+
# @param context [String, Symbol, #to_s] a context name, such as `"File"`
|
|
115
|
+
# @return [String] the reference page's filename for that context
|
|
76
116
|
def page_name(context) = "#{Naming.snake(context)}.md"
|
|
77
117
|
|
|
78
118
|
# Every reference page, rendered fresh — prose carried over from
|
|
79
119
|
# the committed pages, new words seeded with the sentinel, orphaned
|
|
80
120
|
# prose refused.
|
|
121
|
+
#
|
|
122
|
+
# @param directory [String] path to the directory holding the committed
|
|
123
|
+
# reference pages, read for their hand-written prose
|
|
124
|
+
# @return [Hash{String => String}] every page's filename (plus `"index.md"`)
|
|
125
|
+
# mapped to its freshly rendered Markdown content
|
|
126
|
+
# @raise [RuntimeError] if a committed page carries prose for a word the
|
|
127
|
+
# language no longer declares in that context
|
|
81
128
|
def pages(directory)
|
|
82
129
|
contexts.each_with_object({}) do |context, pages|
|
|
83
130
|
path = File.join(directory, page_name(context))
|
|
@@ -86,13 +133,22 @@ module Hecks
|
|
|
86
133
|
end.merge("index.md" => render_index)
|
|
87
134
|
end
|
|
88
135
|
|
|
89
|
-
# A
|
|
136
|
+
# A word admitting two forms has two rows — syntax.bluebook's own
|
|
90
137
|
# stated rule, and `identified_by` (a block, or a bare argument and
|
|
91
138
|
# none) is the case that made it real again. One section per word all
|
|
92
139
|
# the same: the prose is the word's rather than the form's, and the
|
|
93
140
|
# argument rows join by (word, context) and so already cover every
|
|
94
141
|
# form. Grouped rather than rendered per row, or a reader would meet
|
|
95
142
|
# the same heading and the same paragraph twice.
|
|
143
|
+
#
|
|
144
|
+
# @param context [String] the context name, such as `"File"`
|
|
145
|
+
# @param prose [Hash{String, Symbol => String}] hand-written prose harvested
|
|
146
|
+
# from the committed page, keyed by word (or `PREAMBLE` for the page's lede)
|
|
147
|
+
# @param path [String] the page's file path, used only in the orphan-refusal
|
|
148
|
+
# message below
|
|
149
|
+
# @return [String] the page's full rendered Markdown
|
|
150
|
+
# @raise [RuntimeError] if `prose` carries a key for a word the language no
|
|
151
|
+
# longer declares in `context`
|
|
96
152
|
def render_page(context, prose, path)
|
|
97
153
|
words = keywords.select { |row| row[:context] == context }.group_by { |row| row[:word] }
|
|
98
154
|
orphans = prose.keys - words.keys - [PREAMBLE]
|
|
@@ -119,6 +175,10 @@ module Hecks
|
|
|
119
175
|
PAGE
|
|
120
176
|
end
|
|
121
177
|
|
|
178
|
+
# The one-line description of where a context's words are typed.
|
|
179
|
+
#
|
|
180
|
+
# @param context [String] the context name, such as `"File"` or `"Command"`
|
|
181
|
+
# @return [String] a sentence naming where words in this context are typed
|
|
122
182
|
def context_lede(context)
|
|
123
183
|
openers = keywords.select { |row| row[:opens] == context }
|
|
124
184
|
return "Words available at the top of a file." if context == "File"
|
|
@@ -131,6 +191,12 @@ module Hecks
|
|
|
131
191
|
# One spelling per form, everything else off the first row — the
|
|
132
192
|
# columns that differ between two forms of one word are `body` (which
|
|
133
193
|
# is what the spelling shows) and nothing else.
|
|
194
|
+
#
|
|
195
|
+
# @param forms [Array<Hash{Symbol => String}>] one word's Keyword rows, one
|
|
196
|
+
# per admitted form
|
|
197
|
+
# @param prose [String, nil] the word's hand-written prose, or nil if none
|
|
198
|
+
# was harvested
|
|
199
|
+
# @return [String] the word's rendered section, generated table plus prose
|
|
134
200
|
def render_word(forms, prose)
|
|
135
201
|
row = forms.first
|
|
136
202
|
table = argument_table(row)
|
|
@@ -152,15 +218,30 @@ module Hecks
|
|
|
152
218
|
WORD
|
|
153
219
|
end
|
|
154
220
|
|
|
221
|
+
# Falls back to the TODO sentinel when a word has no prose yet.
|
|
222
|
+
#
|
|
223
|
+
# @param prose [String, nil] a word's hand-written prose, or nil if none
|
|
224
|
+
# was harvested
|
|
225
|
+
# @return [String] `prose` stripped, or the TODO sentinel if it is blank
|
|
155
226
|
def prose_or_sentinel(prose)
|
|
156
227
|
text = prose.to_s.strip
|
|
157
228
|
text.empty? ? TODO_SENTINEL : text
|
|
158
229
|
end
|
|
159
230
|
|
|
231
|
+
# Finds the Argument rows declared for one Keyword row's word.
|
|
232
|
+
#
|
|
233
|
+
# @param row [Hash{Symbol => String}] a Keyword row
|
|
234
|
+
# @return [Array<Hash{Symbol => String}>] the Argument rows declared for
|
|
235
|
+
# this row's word, in this row's context
|
|
160
236
|
def word_arguments(row)
|
|
161
237
|
arguments.select { |arg| arg[:keyword] == row[:word] && arg[:context] == row[:context] }
|
|
162
238
|
end
|
|
163
239
|
|
|
240
|
+
# Builds the call spelling shown for one word.
|
|
241
|
+
#
|
|
242
|
+
# @param row [Hash{Symbol => String}] a Keyword row
|
|
243
|
+
# @return [String] the word's call spelling, positional arguments then
|
|
244
|
+
# named ones, with a trailing `do ... end` unless its body is `"none"`
|
|
164
245
|
def signature(row)
|
|
165
246
|
positional = word_arguments(row).reject { |arg| arg[:at].to_s.empty? }
|
|
166
247
|
.sort_by { |arg| arg[:at].to_i }
|
|
@@ -172,6 +253,11 @@ module Hecks
|
|
|
172
253
|
row[:body].to_s == "none" ? base : "#{base} do ... end"
|
|
173
254
|
end
|
|
174
255
|
|
|
256
|
+
# Renders one word's arguments as a Markdown table.
|
|
257
|
+
#
|
|
258
|
+
# @param row [Hash{Symbol => String}] a Keyword row
|
|
259
|
+
# @return [String] a Markdown table of the row's arguments, or `""` if it
|
|
260
|
+
# declares none
|
|
175
261
|
def argument_table(row)
|
|
176
262
|
args = word_arguments(row)
|
|
177
263
|
return "" if args.empty?
|
|
@@ -184,6 +270,10 @@ module Hecks
|
|
|
184
270
|
"#{lines.join("\n")}\n"
|
|
185
271
|
end
|
|
186
272
|
|
|
273
|
+
# Renders the reference index page.
|
|
274
|
+
#
|
|
275
|
+
# @return [String] the reference index page's rendered Markdown, one
|
|
276
|
+
# linked entry per context
|
|
187
277
|
def render_index
|
|
188
278
|
listed = contexts.map do |context|
|
|
189
279
|
count = keywords.select { |row| row[:context] == context }.map { |row| row[:word] }.uniq.size
|
|
@@ -204,7 +294,7 @@ module Hecks
|
|
|
204
294
|
# Prose keyed by word: everything between a section's generated
|
|
205
295
|
# region and the next `## ` heading (or end of file).
|
|
206
296
|
#
|
|
207
|
-
# Starts on PREAMBLE rather than nil so the text between the
|
|
297
|
+
# Starts on `PREAMBLE` rather than nil so the text between the page's
|
|
208
298
|
# own generated lede and its first word heading is carried over too
|
|
209
299
|
# instead of being silently dropped. A page written before that
|
|
210
300
|
# region existed has no generated marker ahead of its first `## `,
|
|
@@ -214,6 +304,11 @@ module Hecks
|
|
|
214
304
|
# buffer/in_fence) — each branch mutates shared local state that
|
|
215
305
|
# carries into the next iteration, so splitting per branch would
|
|
216
306
|
# mean passing all four back and forth by reference every line.
|
|
307
|
+
#
|
|
308
|
+
# @param text [String] a committed reference page's full Markdown source
|
|
309
|
+
# @return [Hash{String, Symbol => String}] hand-written prose, keyed by
|
|
310
|
+
# word (or `PREAMBLE` for the page's lede); empty and TODO-sentinel-only
|
|
311
|
+
# entries are dropped
|
|
217
312
|
# rubocop:disable-next Metrics/PerceivedComplexity
|
|
218
313
|
def harvest(text)
|
|
219
314
|
prose = {}
|
|
@@ -246,6 +341,13 @@ module Hecks
|
|
|
246
341
|
prose.reject { |_word, text_| text_.empty? || text_ == TODO_SENTINEL }
|
|
247
342
|
end
|
|
248
343
|
|
|
344
|
+
# Renders every reference page and writes each to `directory`.
|
|
345
|
+
#
|
|
346
|
+
# @param directory [String] path to the directory to write pages into,
|
|
347
|
+
# created if it does not exist
|
|
348
|
+
# @return [void]
|
|
349
|
+
# @raise [RuntimeError] if a committed page carries prose for a word the
|
|
350
|
+
# language no longer declares in that context
|
|
249
351
|
def write!(directory)
|
|
250
352
|
FileUtils.mkdir_p(directory)
|
|
251
353
|
pages(directory).each do |name, content|
|
|
@@ -257,6 +359,10 @@ module Hecks
|
|
|
257
359
|
# reference page, keyed by region id instead of a word, so the
|
|
258
360
|
# index a reader lands on first can't drift from what actually
|
|
259
361
|
# exists on disk either.
|
|
362
|
+
#
|
|
363
|
+
# @param root [String] the repository root
|
|
364
|
+
# @return [Hash{String => String}] each region id mapped to its freshly
|
|
365
|
+
# rendered content
|
|
260
366
|
def readme_regions(root)
|
|
261
367
|
{
|
|
262
368
|
"guides" => guide_index(root),
|
|
@@ -267,6 +373,11 @@ module Hecks
|
|
|
267
373
|
}
|
|
268
374
|
end
|
|
269
375
|
|
|
376
|
+
# Lists every committed guide, linked and titled by its own heading.
|
|
377
|
+
#
|
|
378
|
+
# @param root [String] the repository root
|
|
379
|
+
# @return [String] a Markdown list linking every committed guide, titled
|
|
380
|
+
# by its own `# ` heading
|
|
270
381
|
def guide_index(root)
|
|
271
382
|
paths = Dir.glob(File.join(root, "docs/implemented/guides/*.md"))
|
|
272
383
|
.reject { |p| %w[AUTHORING.md].include?(File.basename(p)) }
|
|
@@ -278,6 +389,11 @@ module Hecks
|
|
|
278
389
|
lines.join("\n")
|
|
279
390
|
end
|
|
280
391
|
|
|
392
|
+
# Links the reference index, with its context count.
|
|
393
|
+
#
|
|
394
|
+
# @param _root [String] unused; kept for the shape `readme_regions` calls
|
|
395
|
+
# every region renderer with
|
|
396
|
+
# @return [String] one line linking the reference index, with its context count
|
|
281
397
|
def reference_index(_root)
|
|
282
398
|
count = contexts.size
|
|
283
399
|
"[The DSL reference](docs/implemented/reference/index.md) — #{count} contexts, generated from " \
|
|
@@ -285,6 +401,11 @@ module Hecks
|
|
|
285
401
|
"`spec/reference_golden_spec.rb`."
|
|
286
402
|
end
|
|
287
403
|
|
|
404
|
+
# Lists every `bin/` script that opens with a comment, one row each.
|
|
405
|
+
#
|
|
406
|
+
# @param root [String] the repository root
|
|
407
|
+
# @return [String] a Markdown table of every `bin/` script with an opening
|
|
408
|
+
# comment, one row each
|
|
288
409
|
def tool_table(root)
|
|
289
410
|
scripts = Dir.glob(File.join(root, "bin/*")).select { |p| File.file?(p) }.sort
|
|
290
411
|
rows = scripts.filter_map { |path| [path, tool_summary(path)] }.select { |_, desc| desc }
|
|
@@ -298,6 +419,10 @@ module Hecks
|
|
|
298
419
|
# little long and says "...". A code-bearing comment (`field.name`,
|
|
299
420
|
# `pattern:`) makes naive sentence-splitting on "." or ":" cut in
|
|
300
421
|
# the wrong place, so this truncates on length alone.
|
|
422
|
+
#
|
|
423
|
+
# @param path [String] a `bin/` script's path
|
|
424
|
+
# @return [String, nil] its opening comment paragraph, truncated to 140
|
|
425
|
+
# characters, or nil if the script opens with no comment
|
|
301
426
|
def tool_summary(path)
|
|
302
427
|
comment_lines = []
|
|
303
428
|
started = false
|
|
@@ -324,6 +449,10 @@ module Hecks
|
|
|
324
449
|
# own drift check; this just quotes its own output, so the two
|
|
325
450
|
# can't independently drift from each other either — a stale
|
|
326
451
|
# Order_lifecycle.mmd fails that spec long before this one runs.
|
|
452
|
+
#
|
|
453
|
+
# @param root [String] the repository root
|
|
454
|
+
# @return [String] a Markdown section showcasing the generated diagram
|
|
455
|
+
# tooling, quoting `examples/pizzas`'s own committed Order lifecycle diagram
|
|
327
456
|
def diagram_showcase(root)
|
|
328
457
|
lifecycle = File.read(File.join(root, "docs/generated/diagrams/pizzas/Order_lifecycle.mmd")).strip
|
|
329
458
|
<<~MARKDOWN.strip
|
|
@@ -337,6 +466,11 @@ module Hecks
|
|
|
337
466
|
MARKDOWN
|
|
338
467
|
end
|
|
339
468
|
|
|
469
|
+
# Lists every example domain, with its own declared vision.
|
|
470
|
+
#
|
|
471
|
+
# @param root [String] the repository root
|
|
472
|
+
# @return [String] a Markdown list of every example domain with a
|
|
473
|
+
# `.bluebook` file, each with its own declared `vision` text
|
|
340
474
|
def corpus_roster(root)
|
|
341
475
|
dirs = Dir.glob(File.join(root, "examples/*/"))
|
|
342
476
|
lines = dirs.filter_map do |dir|
|
|
@@ -351,6 +485,12 @@ module Hecks
|
|
|
351
485
|
lines.join("\n")
|
|
352
486
|
end
|
|
353
487
|
|
|
488
|
+
# Replaces every generated region inside `text` with its freshly rendered
|
|
489
|
+
# content, leaving the hand-written parts of README untouched.
|
|
490
|
+
#
|
|
491
|
+
# @param root [String] the repository root
|
|
492
|
+
# @param text [String] the README's current full text
|
|
493
|
+
# @return [String] the README's text, with each generated region refreshed
|
|
354
494
|
def render_readme(root, text)
|
|
355
495
|
readme_regions(root).reduce(text) do |current, (id, content)|
|
|
356
496
|
pattern = /#{Regexp.escape(region_begin(id))}.*?#{Regexp.escape(GENERATED_END)}/m
|
|
@@ -358,6 +498,10 @@ module Hecks
|
|
|
358
498
|
end
|
|
359
499
|
end
|
|
360
500
|
|
|
501
|
+
# Regenerates README's generated regions in place.
|
|
502
|
+
#
|
|
503
|
+
# @param root [String] the repository root
|
|
504
|
+
# @return [void]
|
|
361
505
|
def write_readme!(root)
|
|
362
506
|
path = File.join(root, "README.md")
|
|
363
507
|
File.write(path, render_readme(root, File.read(path)))
|
|
@@ -371,9 +515,14 @@ module Hecks
|
|
|
371
515
|
# gate exists to refuse.
|
|
372
516
|
EXAMPLE_FENCE = /^```ruby(?: bluebook| boot)?[ \t]*$/
|
|
373
517
|
|
|
518
|
+
# Whether a word's prose carries a runnable example.
|
|
519
|
+
#
|
|
520
|
+
# @param prose [String, nil] a word's hand-written prose, or nil
|
|
521
|
+
# @return [Boolean] true if `prose` contains a runnable `ruby` or
|
|
522
|
+
# `ruby bluebook`/`ruby boot` fenced example
|
|
374
523
|
def exemplified?(prose) = prose.to_s.match?(EXAMPLE_FENCE)
|
|
375
524
|
|
|
376
|
-
# Every live
|
|
525
|
+
# Every live word, paired with its prose. Both coverage gates ask a
|
|
377
526
|
# question about this same walk and differ only in what they ask of
|
|
378
527
|
# the prose, so they share it rather than each re-deriving the page
|
|
379
528
|
# set — the two are meant to move together, and one drifting past
|
|
@@ -382,6 +531,12 @@ module Hecks
|
|
|
382
531
|
#
|
|
383
532
|
# `harvest` already rejects empty prose and the TODO sentinel, so a
|
|
384
533
|
# word with nothing written for it arrives here with a nil.
|
|
534
|
+
#
|
|
535
|
+
# @param directory [String] path to the directory holding the committed
|
|
536
|
+
# reference pages
|
|
537
|
+
# @return [Array(String, String, String), Array(String, String, nil)]
|
|
538
|
+
# `[word, context, prose]` for every live (admitted or deprecated) word,
|
|
539
|
+
# `prose` nil if none was harvested
|
|
385
540
|
def live_words(directory)
|
|
386
541
|
rows = contexts.flat_map do |context|
|
|
387
542
|
path = File.join(directory, page_name(context))
|
|
@@ -392,9 +547,18 @@ module Hecks
|
|
|
392
547
|
rows.uniq { |word, context, _| [word, context] }
|
|
393
548
|
end
|
|
394
549
|
|
|
550
|
+
# Disambiguates a word by the context it is declared in.
|
|
551
|
+
#
|
|
552
|
+
# @param word [String] a word
|
|
553
|
+
# @param context [String] the context it is declared in
|
|
554
|
+
# @return [String] the word, disambiguated by its context
|
|
395
555
|
def name_of(word, context) = "#{word} (#{context})"
|
|
396
556
|
|
|
397
557
|
# The coverage gate's question: every live word with no prose yet.
|
|
558
|
+
#
|
|
559
|
+
# @param directory [String] path to the directory holding the committed
|
|
560
|
+
# reference pages
|
|
561
|
+
# @return [Array<String>] each undocumented live word, named by `name_of`
|
|
398
562
|
def undocumented(directory)
|
|
399
563
|
live_words(directory).reject { |_word, _context, prose| prose }
|
|
400
564
|
.map { |word, context, _| name_of(word, context) }
|
|
@@ -407,6 +571,11 @@ module Hecks
|
|
|
407
571
|
# shipped twice (`read_model`'s where/order_by/limit/offset, and
|
|
408
572
|
# `role`/`goal` on a command). An example that runs is the only
|
|
409
573
|
# documentation that can go red.
|
|
574
|
+
#
|
|
575
|
+
# @param directory [String] path to the directory holding the committed
|
|
576
|
+
# reference pages
|
|
577
|
+
# @return [Array<String>] each live word with prose but no runnable
|
|
578
|
+
# example, named by `name_of`
|
|
410
579
|
def unexemplified(directory)
|
|
411
580
|
live_words(directory).reject { |_word, _context, prose| exemplified?(prose) }
|
|
412
581
|
.map { |word, context, _| name_of(word, context) }
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
module Hecks
|
|
2
|
-
#
|
|
3
|
-
# for members that don't ship inside hecks's own lib/ at all: a
|
|
4
|
-
# separate, independently-versioned package
|
|
5
|
-
# (github.com/chrisyoung/embryonaut_bluebooks) that a consuming project
|
|
6
|
-
# vendors into its own checkout, the same way a project already vendors
|
|
7
|
-
# hecks itself (bin/vendor_hecks, vendor/hecks/).
|
|
2
|
+
# A vendored, external bluebook.
|
|
8
3
|
#
|
|
9
|
-
#
|
|
4
|
+
# ## What it is
|
|
5
|
+
#
|
|
6
|
+
# Same shape as Framework (framework.rb), for members that don't ship
|
|
7
|
+
# inside hecks's own lib/ at all: a separate, independently-versioned
|
|
8
|
+
# package (github.com/chrisyoung/embryonaut_bluebooks) that a consuming
|
|
9
|
+
# project vendors into its own checkout, the same way a project already
|
|
10
|
+
# vendors hecks itself (bin/vendor_hecks, vendor/hecks/).
|
|
11
|
+
#
|
|
12
|
+
# ## Recovered, not rebuilt
|
|
13
|
+
#
|
|
14
|
+
# This module and its `uses_embryonaut_bluebook`
|
|
10
15
|
# DSL word (hecksagon_builder.rb) were built on a prior commit of this
|
|
11
16
|
# repo (933d1dd), vendored out to a real consumer (lifeadelics/domain,
|
|
12
17
|
# for embryonaut_bluebooks/payments), and then lost from this repo's own
|
|
@@ -17,6 +22,8 @@ module Hecks
|
|
|
17
22
|
# conventions rather than copied wholesale, since the two trees had
|
|
18
23
|
# otherwise diverged for weeks in both directions.
|
|
19
24
|
#
|
|
25
|
+
# ## Resolution
|
|
26
|
+
#
|
|
20
27
|
# Resolved from the consuming registry's own root, not this gem's
|
|
21
28
|
# __dir__ — Framework::ROOT can be a fixed, `__dir__`-relative constant
|
|
22
29
|
# because framework members ship inside this gem; an embryonaut bluebook
|
|
@@ -27,6 +34,8 @@ module Hecks
|
|
|
27
34
|
# itself only runs at hecksagon-build time, when a real registry is
|
|
28
35
|
# current.
|
|
29
36
|
#
|
|
37
|
+
# ## Load order
|
|
38
|
+
#
|
|
30
39
|
# Every `.bluebook` file in the package, sorted — not just one. Unlike a
|
|
31
40
|
# framework member (one file, named by its own stem), a vendored package
|
|
32
41
|
# can span several bluebook files that reopen the same `Hecks.bluebook`
|
|
@@ -37,7 +46,9 @@ module Hecks
|
|
|
37
46
|
# payment < payments < policies, the same reason that package's own
|
|
38
47
|
# files are named to fall in that order in the first place.
|
|
39
48
|
#
|
|
40
|
-
#
|
|
49
|
+
# ## Scope
|
|
50
|
+
#
|
|
51
|
+
# Only the bluebook files — same restriction Framework draws, same
|
|
41
52
|
# reason: a `.hecksagon`/`.port`/`.adapter` is a wiring decision
|
|
42
53
|
# (persistence, which processor adapter is bound) that belongs to
|
|
43
54
|
# whoever is deploying, never baked into the vendored package itself.
|
|
@@ -46,12 +57,24 @@ module Hecks
|
|
|
46
57
|
# `Hecks.hecksagon "Payments" do ... end` to bind real storage/adapters
|
|
47
58
|
# — see Framework's own comment for the fuller reasoning, identical here.
|
|
48
59
|
#
|
|
49
|
-
#
|
|
60
|
+
# ## Idempotency
|
|
61
|
+
#
|
|
62
|
+
# Idempotent the same way `Framework.load!` is — checked against the
|
|
50
63
|
# bluebook this package actually declares (`Naming.pascal("payments")`
|
|
51
64
|
# => "Payments"), not a separate ledger. A vendored package's directory
|
|
52
65
|
# name and its declared `Hecks.bluebook` name are the one convention
|
|
53
66
|
# this reuses from Framework rather than reinventing.
|
|
54
67
|
module EmbryonautBluebook
|
|
68
|
+
# Loads a vendored embryonaut bluebook package's `.bluebook` files, once
|
|
69
|
+
# per registry.
|
|
70
|
+
#
|
|
71
|
+
# @param name [String, Symbol] the vendored package's directory name, such
|
|
72
|
+
# as `"payments"`
|
|
73
|
+
# @param registry [Runtime::Registry, nil] the registry to vendor into and
|
|
74
|
+
# check for an existing load; defaults to the current boot registry
|
|
75
|
+
# @return [void]
|
|
76
|
+
# @raise [Runtime::WiringError] if `registry` has no root, or no vendored
|
|
77
|
+
# package named `name` is checked out
|
|
55
78
|
def self.load!(name, registry: Hecks.current_registry)
|
|
56
79
|
unless registry&.root
|
|
57
80
|
raise Runtime::WiringError,
|
data/lib/hecks/facade/handle.rb
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
require_relative "../naming"
|
|
2
|
+
require_relative "../runtime/caller"
|
|
3
|
+
require_relative "../ports/authorization"
|
|
2
4
|
|
|
3
5
|
module Hecks
|
|
4
6
|
module Facade
|
|
@@ -41,7 +43,7 @@ module Hecks
|
|
|
41
43
|
# @param key [Symbol, String] the attribute name
|
|
42
44
|
# @return [Object, nil] the value held in state (a scalar, a value object, a list,
|
|
43
45
|
# or a referenced record's id); `nil` when the field is unset or not in state
|
|
44
|
-
def [](key) =
|
|
46
|
+
def [](key) = redacted(key.to_sym)
|
|
45
47
|
|
|
46
48
|
# Answers the record's state as a plain Hash with the bare identity under `:id`.
|
|
47
49
|
#
|
|
@@ -59,7 +61,7 @@ module Hecks
|
|
|
59
61
|
#
|
|
60
62
|
# @return [Hash{Symbol => Object}] a new Hash of every state field by attribute
|
|
61
63
|
# name, plus `:id` holding the identity String
|
|
62
|
-
def to_h = @state.merge(id: @id)
|
|
64
|
+
def to_h = @state.to_h { |key, _| [key, redacted(key)] }.merge(id: @id)
|
|
63
65
|
|
|
64
66
|
# Names the aggregate this record belongs to, in the form every dispatch verb and
|
|
65
67
|
# event is addressed by.
|
|
@@ -120,7 +122,7 @@ module Hecks
|
|
|
120
122
|
# written yet
|
|
121
123
|
# @raise [NoMethodError] if `name` is neither a key in state nor a declared field
|
|
122
124
|
def method_missing(name, *args, **kwargs, &)
|
|
123
|
-
return
|
|
125
|
+
return redacted(name) if @state.key?(name) || reader?(name)
|
|
124
126
|
|
|
125
127
|
super
|
|
126
128
|
end
|
|
@@ -133,6 +135,77 @@ module Hecks
|
|
|
133
135
|
|
|
134
136
|
def repository = @dispatcher.registry.repository(@domain, @ir)
|
|
135
137
|
|
|
138
|
+
# One field's value, with any Privacy::Marking-flagged leaf masked
|
|
139
|
+
# out unless the ambient caller holds a live Governance grant of
|
|
140
|
+
# the marking's own `readable_by` — the read-side half of the
|
|
141
|
+
# Privacy framework member (lib/hecks/framework/bluebook/
|
|
142
|
+
# privacy.bluebook): a marking's presence is what makes a read
|
|
143
|
+
# redacted, not a separate flag this class carries itself.
|
|
144
|
+
#
|
|
145
|
+
# ALWAYS THE STRONG CHECK, never the weak string-only fallback
|
|
146
|
+
# `CommandRules::Authorization#refuse_role_mismatch` allows an
|
|
147
|
+
# unidentified caller — a read gate gone wrong is a leak, not a
|
|
148
|
+
# refused command, so an ambient caller with no `actor_id` (or no
|
|
149
|
+
# caller at all) is masked here, full stop, rather than waved
|
|
150
|
+
# through the way a self-asserted `role` string is for a command.
|
|
151
|
+
#
|
|
152
|
+
# ONE LEVEL OF NESTING ONLY — `attendee.medications` masks inside
|
|
153
|
+
# the returned `Runtime::Value` via its own `#with`; a marking two
|
|
154
|
+
# levels deep is not supported and is left unmasked rather than
|
|
155
|
+
# silently mishandled, since nothing in this corpus needs it yet.
|
|
156
|
+
#
|
|
157
|
+
# @param field [Symbol] the state key being read
|
|
158
|
+
# @return [Object, nil] `@state[field]`, or a copy with the marked leaf replaced by
|
|
159
|
+
# the literal String `"[redacted]"` when the caller is not authorized to see it
|
|
160
|
+
def redacted(field)
|
|
161
|
+
raw = @state[field]
|
|
162
|
+
rows = marked_paths.select { |row| row[:attribute_path][:value].to_s.split(".", 2).first == field.to_s }
|
|
163
|
+
return raw if rows.empty?
|
|
164
|
+
|
|
165
|
+
rows.each do |row|
|
|
166
|
+
path = row[:attribute_path][:value].to_s
|
|
167
|
+
next if authorized_for?(row[:readable_by][:value].to_s)
|
|
168
|
+
|
|
169
|
+
segments = path.split(".", 2)
|
|
170
|
+
if segments.size == 1
|
|
171
|
+
raw = "[redacted]"
|
|
172
|
+
elsif raw.is_a?(Runtime::Value)
|
|
173
|
+
raw = raw.with(segments[1], "[redacted]")
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
raw
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Every Privacy::Marking declared for this record's own aggregate — `[]` when the
|
|
181
|
+
# Privacy framework member is not attached in this boot at all, checked once and
|
|
182
|
+
# cheaply rather than paying for a query dispatch every read on a domain that never
|
|
183
|
+
# attached Privacy.
|
|
184
|
+
#
|
|
185
|
+
# @return [Array<Hash>] `Privacy::Marking.ForDomain`'s own rows for this `fqn`
|
|
186
|
+
def marked_paths
|
|
187
|
+
return @marked_paths if defined?(@marked_paths)
|
|
188
|
+
return @marked_paths = [] unless @dispatcher.registry.bluebook("Privacy")
|
|
189
|
+
|
|
190
|
+
@marked_paths = @dispatcher.query("Privacy::Marking.ForDomain", domain: fqn)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Whether the ambient caller holds a live Governance grant of `role`, over this
|
|
194
|
+
# record's own domain — `false` outright for an unidentified caller or a domain
|
|
195
|
+
# with no authorization provider attached, never the weak fallback a command's own
|
|
196
|
+
# role check allows (see `redacted`'s own header for why).
|
|
197
|
+
#
|
|
198
|
+
# @param role [String] the marking's own `readable_by`
|
|
199
|
+
# @return [Boolean] true only for an identified caller holding a live grant
|
|
200
|
+
def authorized_for?(role)
|
|
201
|
+
caller = Runtime::Caller.current
|
|
202
|
+
return false unless caller&.actor_id
|
|
203
|
+
return false unless @dispatcher.registry.authorization_provider_for(@domain)
|
|
204
|
+
|
|
205
|
+
Ports::Authorization.holds_role?(@dispatcher.registry, actor_id: caller.actor_id, role: role,
|
|
206
|
+
as_of: caller.as_of, scope: caller.scope)
|
|
207
|
+
end
|
|
208
|
+
|
|
136
209
|
def reader?(name)
|
|
137
210
|
!@ir.attribute(name).nil? || @ir.lifecycle&.field&.to_sym == name
|
|
138
211
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
require_relative "../../bluebook/dsl/hecksagon_builder"
|
|
2
2
|
require_relative "../../bluebook/dsl/domain_port_builder"
|
|
3
3
|
require_relative "../../bluebook/dsl/const_shim"
|
|
4
|
+
require_relative "../../bluebook/dsl/binding_proxy"
|
|
4
5
|
require_relative "../../bluebook/hexagon"
|
|
5
6
|
require_relative "../handle"
|
|
6
7
|
require_relative "../../naming"
|
|
@@ -164,6 +165,13 @@ module Hecks
|
|
|
164
165
|
end
|
|
165
166
|
|
|
166
167
|
door.define_singleton_method(:method_missing) do |verb, *args, **kwargs, &block|
|
|
168
|
+
# A BARE CALL starts a Privacy marking chain — see
|
|
169
|
+
# `Bluebook::DSL::BindingProxy#method_missing`'s own header;
|
|
170
|
+
# this is the same mechanism, reached when the constant is
|
|
171
|
+
# already a real, installed door (a second boot in-process)
|
|
172
|
+
# rather than a `.hecksagon`-parse-time `BindingProxy`.
|
|
173
|
+
return Bluebook::DSL::AttributePath.new(fqn, [verb.to_s]) if args.empty? && kwargs.empty? && !block
|
|
174
|
+
|
|
167
175
|
collector = Bluebook::DSL::HecksagonBuilder.collector
|
|
168
176
|
return super(verb, *args, **kwargs, &block) unless collector
|
|
169
177
|
|
|
@@ -4,6 +4,9 @@ require_relative "../naming"
|
|
|
4
4
|
require_relative "value_object_shape"
|
|
5
5
|
|
|
6
6
|
module Hecks
|
|
7
|
+
# The forms surface's own namespace — the `expose` DSL, the IR->HTML
|
|
8
|
+
# renderers, and the Rack app bridging them; see `forms.rb`'s own header,
|
|
9
|
+
# its primary opening, for the fuller design this implements.
|
|
7
10
|
module Forms
|
|
8
11
|
# One resolved field, ready for a renderer to turn into markup. A leaf
|
|
9
12
|
# scalar carries `kind`/`options`/etc; a `:group` or `:list` carries
|
data/lib/hecks/forms/page.rb
CHANGED
|
@@ -9,6 +9,15 @@ module Hecks
|
|
|
9
9
|
# `prefers-color-scheme` alone; nothing here reads a cookie or a query
|
|
10
10
|
# param for it, so it is never wrong for the browser rendering it.
|
|
11
11
|
module Page
|
|
12
|
+
# Wraps one page's own body HTML in the shared shell: doctype, head, nav, footer,
|
|
13
|
+
# inline styles and script.
|
|
14
|
+
#
|
|
15
|
+
# @param title [String] the page title, escaped into `<title>` and the browser tab
|
|
16
|
+
# @param body [String] the page's own body markup, inserted unescaped inside `<main>`
|
|
17
|
+
# @param breadcrumbs [Array<Array(String, String), Array(String, nil)>] each
|
|
18
|
+
# `[label, href]` pair, in order; the last pair's `href` should be `nil` for the
|
|
19
|
+
# current page
|
|
20
|
+
# @return [String] the complete HTML document
|
|
12
21
|
def self.render(title:, body:, breadcrumbs: [])
|
|
13
22
|
<<~HTML
|
|
14
23
|
<!doctype html>
|
|
@@ -36,6 +45,11 @@ module Hecks
|
|
|
36
45
|
HTML
|
|
37
46
|
end
|
|
38
47
|
|
|
48
|
+
# Renders the breadcrumb trail as a `<nav>`, each crumb a link except the last.
|
|
49
|
+
#
|
|
50
|
+
# @param crumbs [Array<Array(String, String), Array(String, nil)>] each
|
|
51
|
+
# `[label, href]` pair; a `nil` href renders as the current, unlinked page
|
|
52
|
+
# @return [String] the `<nav>` markup, HTML-escaped; `""` when `crumbs` is empty
|
|
39
53
|
def self.breadcrumbs_html(crumbs)
|
|
40
54
|
return "" if crumbs.empty?
|
|
41
55
|
|