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/framework.rb
CHANGED
|
@@ -6,6 +6,8 @@ module Hecks
|
|
|
6
6
|
# domain-agnostic chapters no single example owns, shared by reference
|
|
7
7
|
# rather than copied into every domain that wants one.
|
|
8
8
|
#
|
|
9
|
+
# ## Where it lives
|
|
10
|
+
#
|
|
9
11
|
# Under `lib/`, not a top-level sibling — a real consumer (embryonaut)
|
|
10
12
|
# vendors only `lib/` (`bin/vendor-hecks`, and this gem's own
|
|
11
13
|
# `hecks.gemspec`, both glob `lib/**/*`), so a `framework/`
|
|
@@ -19,6 +21,8 @@ module Hecks
|
|
|
19
21
|
# non-Ruby data a module owns lives inside `lib/` with the code that
|
|
20
22
|
# reads it, not beside it.
|
|
21
23
|
#
|
|
24
|
+
# ## Derived, not hand-listed
|
|
25
|
+
#
|
|
22
26
|
# Derived from the directory, not hand-listed a second time — the same
|
|
23
27
|
# reasoning `Assembly::CONTRACTS` gives for reading the language's own
|
|
24
28
|
# fields from a table instead of restating them: a member added here
|
|
@@ -27,19 +31,13 @@ module Hecks
|
|
|
27
31
|
# stale in silence. `spec/corpus_spec.rb`'s own `FRAMEWORK_MEMBERS`
|
|
28
32
|
# glob is the precedent this mirrors.
|
|
29
33
|
#
|
|
34
|
+
# ## Naming
|
|
35
|
+
#
|
|
30
36
|
# Named by file stem, capitalized — `governance.bluebook` holds
|
|
31
37
|
# `Hecks.bluebook "Governance"`, the same one-to-one spelling every
|
|
32
38
|
# other chapter in this codebase already keeps between its filename
|
|
33
39
|
# and its declared name.
|
|
34
40
|
module Framework
|
|
35
|
-
ROOT = File.expand_path("framework/bluebook", __dir__).freeze
|
|
36
|
-
|
|
37
|
-
def self.members
|
|
38
|
-
Dir.glob(File.join(ROOT, "*.bluebook")).to_h do |path|
|
|
39
|
-
[Naming.pascal(File.basename(path, ".bluebook")), path]
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
|
|
43
41
|
# Loaded from its own real path, always — never a copy. A domain
|
|
44
42
|
# booted through `Fuzzing::IsolatedBoot`'s tmp-directory copy still
|
|
45
43
|
# reaches the same `framework/bluebook/governance.bluebook` this
|
|
@@ -64,28 +62,36 @@ module Hecks
|
|
|
64
62
|
# everywhere else too. See `examples/banking/bluebook/banking.hecksagon`
|
|
65
63
|
# for the pattern — a real `.hecksagon` file can hold more than one
|
|
66
64
|
# `Hecks.hecksagon` call, one per domain it wires.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
#
|
|
70
|
-
#
|
|
71
|
-
#
|
|
72
|
-
#
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
65
|
+
ROOT = File.expand_path("framework/bluebook", __dir__).freeze
|
|
66
|
+
|
|
67
|
+
# Every framework member available to attach, by declared name.
|
|
68
|
+
#
|
|
69
|
+
# @return [Hash{String => String}] each member's capitalized bluebook name,
|
|
70
|
+
# mapped to the absolute path of its `.bluebook` file
|
|
71
|
+
def self.members
|
|
72
|
+
Dir.glob(File.join(ROOT, "*.bluebook")).to_h do |path|
|
|
73
|
+
[Naming.pascal(File.basename(path, ".bluebook")), path]
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
79
77
|
# Every member whose own bluebook declares `provides capability` —
|
|
80
78
|
# read off the member's real IR (each loaded into a scratch registry,
|
|
81
79
|
# never the caller's), not off its name. Used where a check needs to
|
|
82
80
|
# say which member would satisfy it (`refuse_ungoverned_roles!`'s own
|
|
83
81
|
# suggestion, `Fuzzing::TargetCapabilities`).
|
|
82
|
+
#
|
|
83
|
+
# @param capability [String, Symbol] the capability name to look for
|
|
84
|
+
# @return [Array<String>] the names of every framework member that provides it,
|
|
85
|
+
# sorted
|
|
84
86
|
def self.providers_of(capability)
|
|
85
87
|
members.keys.select { |name| chapter(name).provides?(capability) }.sort
|
|
86
88
|
end
|
|
87
89
|
|
|
88
90
|
# One member's chapter, built in isolation.
|
|
91
|
+
#
|
|
92
|
+
# @param name [String, Symbol] the member's name, such as `"Governance"`
|
|
93
|
+
# @return [Bluebook::Chapter] the member's chapter, loaded into a scratch registry
|
|
94
|
+
# @raise [Runtime::WiringError] if no framework member has that name
|
|
89
95
|
def self.chapter(name)
|
|
90
96
|
path = members.fetch(name.to_s) do
|
|
91
97
|
raise Runtime::WiringError,
|
|
@@ -107,6 +113,23 @@ module Hecks
|
|
|
107
113
|
registry.bluebook(name.to_s)
|
|
108
114
|
end
|
|
109
115
|
|
|
116
|
+
# Idempotent, per registry — `uses_framework` is now called more than
|
|
117
|
+
# once for the same member within a single boot (S8: a domain
|
|
118
|
+
# attaching Governance for its own role check, plus a framework
|
|
119
|
+
# sibling attaching it too, e.g. `banking.hecksagon`'s Identity
|
|
120
|
+
# block). `Kernel.load` always re-executes the file, unlike
|
|
121
|
+
# `require`, so a second call would re-run `Hecks.bluebook
|
|
122
|
+
# "Governance"` a second time — and the self-hosting meta-domain
|
|
123
|
+
# records every declaration as a real dispatched command against its
|
|
124
|
+
# own ledger, so a second `Declare` for the same aggregate is a real
|
|
125
|
+
# `AlreadyExists`, not a no-op. Skipped once the member's bluebook is
|
|
126
|
+
# already registered in this registry — the same chapter, not merely
|
|
127
|
+
# a same-named one from a stale prior boot.
|
|
128
|
+
#
|
|
129
|
+
# @param name [String, Symbol] the member's name, such as `"Governance"`
|
|
130
|
+
# @return [Boolean, nil] true when this call loaded the member's bluebook, nil when
|
|
131
|
+
# the current registry already held it
|
|
132
|
+
# @raise [Runtime::WiringError] if no framework member has that name
|
|
110
133
|
def self.load!(name)
|
|
111
134
|
path = members.fetch(name.to_s) do
|
|
112
135
|
raise Runtime::WiringError,
|
data/lib/hecks/freezer.rb
CHANGED
|
@@ -8,7 +8,7 @@ module Hecks
|
|
|
8
8
|
# Hash a field holds stays mutable, so a caller reaches straight
|
|
9
9
|
# through and edits in place. Each fix looked complete and none was.
|
|
10
10
|
#
|
|
11
|
-
# What should be frozen, and why it is not everything
|
|
11
|
+
# ## What should be frozen, and why it is not everything
|
|
12
12
|
#
|
|
13
13
|
# a value object has no identity to change over. `with` already
|
|
14
14
|
# answers a new one rather than mutating, so freezing it through is
|
|
@@ -33,6 +33,10 @@ module Hecks
|
|
|
33
33
|
# Numbers, symbols, nil and booleans are already immediate or frozen;
|
|
34
34
|
# a Value froze itself when it was built. What is left is the mutable
|
|
35
35
|
# trio, and each has to be walked rather than topped.
|
|
36
|
+
#
|
|
37
|
+
# @param held [Object] any domain value
|
|
38
|
+
# @return [Object] `held`, recursively frozen if it is a Hash, Array, or
|
|
39
|
+
# String; returned unchanged otherwise
|
|
36
40
|
def deep(held)
|
|
37
41
|
case held
|
|
38
42
|
when Hash then held.each_value { |inner| deep(inner) }.freeze
|
|
@@ -45,11 +49,20 @@ module Hecks
|
|
|
45
49
|
# The question a gate asks, rather than the act. Answers false for the
|
|
46
50
|
# first thing that is reachable and mutable, which is what makes a
|
|
47
51
|
# failure message worth reading.
|
|
52
|
+
#
|
|
53
|
+
# @param held [Object] any domain value
|
|
54
|
+
# @return [Boolean] true if `held` and everything reachable from it is frozen
|
|
48
55
|
def deeply_frozen?(held) = unfrozen_within(held).nil?
|
|
49
56
|
|
|
50
57
|
# The path to the first mutable thing reachable from `held`, or nil.
|
|
51
58
|
# A path rather than a boolean because "something in this event is
|
|
52
59
|
# mutable" is not an actionable sentence.
|
|
60
|
+
#
|
|
61
|
+
# @param held [Object] any domain value
|
|
62
|
+
# @param path [Array<String>] the owner path accumulated by the recursive
|
|
63
|
+
# walk so far; callers pass nothing and get the default
|
|
64
|
+
# @return [String, nil] the dotted path (Hash keys, Array indices, or
|
|
65
|
+
# `"(the value itself)"`) to the first mutable value found, or nil if none
|
|
53
66
|
def unfrozen_within(held, path = [])
|
|
54
67
|
return (path.empty? ? "(the value itself)" : path.join(".")) unless immune?(held) || held.frozen?
|
|
55
68
|
|
|
@@ -62,6 +75,9 @@ module Hecks
|
|
|
62
75
|
# Immediates are frozen in every Ruby that matters, but asking
|
|
63
76
|
# `frozen?` of them and trusting the answer has bitten enough people
|
|
64
77
|
# that it is worth being explicit.
|
|
78
|
+
#
|
|
79
|
+
# @param held [Object] any domain value
|
|
80
|
+
# @return [Boolean] true if `held` is nil, true, false, a Numeric, or a Symbol
|
|
65
81
|
def immune?(held) = held.nil? || held == true || held == false || held.is_a?(Numeric) || held.is_a?(Symbol)
|
|
66
82
|
end
|
|
67
83
|
end
|
|
@@ -20,6 +20,8 @@ module Hecks
|
|
|
20
20
|
# `canonical_form.rb` aliasing bug — neither would have been caught by
|
|
21
21
|
# sampling alone, which is the whole reason this phase exists).
|
|
22
22
|
#
|
|
23
|
+
# ## Type-directed generation
|
|
24
|
+
#
|
|
23
25
|
# Type-directed, not exhaustive-over-strings: this generator only ever
|
|
24
26
|
# recurses into a sub-expression whose own type the surrounding
|
|
25
27
|
# construct actually accepts (never `"x".modulo(true)`) — mirroring,
|
|
@@ -36,7 +38,9 @@ module Hecks
|
|
|
36
38
|
# 3" claim for exactly the sublanguage that already bit this project
|
|
37
39
|
# twice.
|
|
38
40
|
#
|
|
39
|
-
#
|
|
41
|
+
# ## What "PROVEN" means here, precisely
|
|
42
|
+
#
|
|
43
|
+
# Every expression this generator
|
|
40
44
|
# produces is well-typed by construction (every sub-expression's type
|
|
41
45
|
# matches what its parent construct actually accepts, per this file's
|
|
42
46
|
# own TYPE_RULES). Interpreting a well-typed expression must never
|
|
@@ -133,20 +137,25 @@ module Hecks
|
|
|
133
137
|
# instance (which responds to `#to_h` but is never itself a bare
|
|
134
138
|
# Hash) down to its lone scalar field, and just as deliberately
|
|
135
139
|
# leaves an actual Hash alone (a genuinely un-hydrated, multi-
|
|
136
|
-
# field record has no single scalar to collapse to). A
|
|
137
|
-
#
|
|
138
|
-
#
|
|
139
|
-
#
|
|
140
|
-
#
|
|
141
|
-
#
|
|
142
|
-
#
|
|
143
|
-
#
|
|
144
|
-
#
|
|
145
|
-
# `false` instead of `true` before this fix.
|
|
140
|
+
# field record has no single scalar to collapse to). A plain
|
|
141
|
+
# `{value: X}` Hash cannot stand in here: `is_a?(Hash)` is true for
|
|
142
|
+
# it, so `unwrap_scalar` leaves it wrapped, and every VO-typed
|
|
143
|
+
# synthetic attribute then fails `Addition`/`Compare`/every
|
|
144
|
+
# scalar-typed operation with "expects a number, got
|
|
145
|
+
# {\"value\":5}" — a bug in this generator's own synthetic state,
|
|
146
|
+
# not in `Resolver`, that would surface only as `num_b == 5`
|
|
147
|
+
# (`num_b` a plain `{value: 5}` Hash) evaluating to `false` instead
|
|
148
|
+
# of `true`.
|
|
146
149
|
SingleFieldVO = Struct.new(:value) do
|
|
147
150
|
def to_h = { value: value }
|
|
148
151
|
end
|
|
149
152
|
|
|
153
|
+
# Builds the fixed state every generated predicate is interpreted against.
|
|
154
|
+
#
|
|
155
|
+
# @return [Hash{Symbol => Object}] symbol-keyed synthetic state: half bare
|
|
156
|
+
# scalars (`num_a`, `str_a`, `bool_a`, half of `arr_num`'s/`arr_str`'s
|
|
157
|
+
# elements), half wrapped `SingleFieldVO` instances (`num_b`, `str_b`,
|
|
158
|
+
# `bool_b`, the other half)
|
|
150
159
|
def synthetic_state
|
|
151
160
|
{
|
|
152
161
|
num_a: 3,
|
|
@@ -160,18 +169,27 @@ module Hecks
|
|
|
160
169
|
}
|
|
161
170
|
end
|
|
162
171
|
|
|
172
|
+
# The `attrs` half of the interpret call — always empty, since every
|
|
173
|
+
# synthetic name this generator uses lives in `synthetic_state` instead.
|
|
174
|
+
#
|
|
175
|
+
# @return [Hash] always `{}`
|
|
163
176
|
def synthetic_attrs = {}
|
|
164
177
|
|
|
165
|
-
#
|
|
166
|
-
#
|
|
167
|
-
#
|
|
168
|
-
#
|
|
169
|
-
#
|
|
170
|
-
#
|
|
178
|
+
# Lists every terminal (non-recursive) expression of `type`: the fixed
|
|
179
|
+
# literal palette plus every synthetic attribute name declared for that
|
|
180
|
+
# type (never their values — this generates text, the same source a
|
|
181
|
+
# real `given`/`invariant` author would write; `synthetic_state`,
|
|
182
|
+
# above, is what gives those names meaning at `interpret` time).
|
|
183
|
+
#
|
|
184
|
+
# @param type [Symbol] one of `TYPES`
|
|
185
|
+
# @return [Array<String>] `type`'s literal palette, synthetic attribute
|
|
186
|
+
# names, and any currently-bound block-parameter leaf for `type`
|
|
171
187
|
def leaves(type)
|
|
172
188
|
(TYPE_LEAVES[type] || []) + (SYNTHETIC_ATTRS[type] || []) + Array(bound_leaves[type])
|
|
173
189
|
end
|
|
174
190
|
|
|
191
|
+
# The per-type stack of currently-bound block-parameter leaf names.
|
|
192
|
+
#
|
|
175
193
|
# A stack, not a single slot — a nested block predicate (the real
|
|
176
194
|
# corpus already does this two levels deep, roster.bluebook's own
|
|
177
195
|
# `seats.any? { |s| assignments.none? { |a| … } }`) pushes a second
|
|
@@ -184,8 +202,20 @@ module Hecks
|
|
|
184
202
|
# outer level already bound, so a shadowed name still interprets
|
|
185
203
|
# correctly — this generator is proving "does it crash," not "is
|
|
186
204
|
# every generated predicate semantically distinct."
|
|
205
|
+
#
|
|
206
|
+
# @return [Hash{Symbol => Array<String>}] each type's own stack of bound
|
|
207
|
+
# leaf names, usually `[]` or `["el"]`
|
|
187
208
|
def bound_leaves = @bound_leaves ||= Hash.new { |h, k| h[k] = [] }
|
|
188
209
|
|
|
210
|
+
# Runs the block with `BLOCK_PARAM` admitted as a leaf of `element_type`,
|
|
211
|
+
# for building a block predicate's own body.
|
|
212
|
+
#
|
|
213
|
+
# @param element_type [Symbol] the type `BLOCK_PARAM` is bound to for the
|
|
214
|
+
# duration of the block
|
|
215
|
+
# @yield runs with the leaf pushed and any cached `element_type` productions
|
|
216
|
+
# invalidated
|
|
217
|
+
# @yieldreturn [Object] the block's own result
|
|
218
|
+
# @return [Object] the block's own result, unchanged
|
|
189
219
|
def with_element_leaf(element_type)
|
|
190
220
|
bound_leaves[element_type] << BLOCK_PARAM
|
|
191
221
|
cache.delete_if { |(type, _depth), _| type == element_type }
|
|
@@ -195,13 +225,18 @@ module Hecks
|
|
|
195
225
|
cache.delete_if { |(type, _depth), _| type == element_type }
|
|
196
226
|
end
|
|
197
227
|
|
|
198
|
-
#
|
|
199
|
-
#
|
|
200
|
-
#
|
|
201
|
-
#
|
|
202
|
-
#
|
|
203
|
-
#
|
|
204
|
-
#
|
|
228
|
+
# Lists every expression of `type` reachable in at most `depth`
|
|
229
|
+
# recursive steps, memoized (the same sub-expression set is reused at
|
|
230
|
+
# every enclosing recursion, so without memoizing, cost would compound
|
|
231
|
+
# multiplicatively per level instead of additively). `depth` 0 is
|
|
232
|
+
# exactly `leaves(type)`; each increment adds every construct this
|
|
233
|
+
# file's own production methods (below) say can produce `type`, built
|
|
234
|
+
# from `depth - 1` sub-expressions.
|
|
235
|
+
#
|
|
236
|
+
# @param type [Symbol] one of `TYPES`
|
|
237
|
+
# @param depth [Integer] maximum recursive steps remaining
|
|
238
|
+
# @return [Array<String>] every expression of `type` reachable in at most
|
|
239
|
+
# `depth` steps, deduplicated
|
|
205
240
|
def productions(type, depth)
|
|
206
241
|
cache[[type, depth]] ||= begin
|
|
207
242
|
base = leaves(type)
|
|
@@ -209,8 +244,18 @@ module Hecks
|
|
|
209
244
|
end
|
|
210
245
|
end
|
|
211
246
|
|
|
247
|
+
# The `productions` memo, keyed by `[type, depth]`.
|
|
248
|
+
#
|
|
249
|
+
# @return [Hash{Array(Symbol, Integer) => Array<String>}] the memoized productions cache
|
|
212
250
|
def cache = @cache ||= {}
|
|
213
251
|
|
|
252
|
+
# Dispatches to the recursive (non-leaf) production rule for `type`.
|
|
253
|
+
#
|
|
254
|
+
# @param type [Symbol] one of `TYPES`
|
|
255
|
+
# @param depth [Integer] maximum recursive steps remaining
|
|
256
|
+
# @return [Array<String>] every non-leaf expression of `type` reachable in at
|
|
257
|
+
# most `depth` steps; `[]` for `:nil_type`, which has no recursive producer
|
|
258
|
+
# @raise [ArgumentError] if `type` names anything outside `TYPES`
|
|
214
259
|
def recursive_productions(type, depth)
|
|
215
260
|
case type
|
|
216
261
|
when :numeric then numeric_productions(depth)
|
|
@@ -237,12 +282,21 @@ module Hecks
|
|
|
237
282
|
# final output) keeps the shape diversity `sample`'s even-spacing
|
|
238
283
|
# already preserves while keeping growth roughly linear in depth
|
|
239
284
|
# instead of combinatorial.
|
|
285
|
+
#
|
|
286
|
+
# @param type [Symbol] one of `TYPES`
|
|
287
|
+
# @param depth [Integer] maximum recursive steps remaining
|
|
288
|
+
# @return [Array<String>] `productions(type, depth)`, sampled down to at most
|
|
289
|
+
# `SAMPLE_CAP` entries
|
|
240
290
|
def bounded(type, depth) = sample(productions(type, depth))
|
|
241
291
|
|
|
242
292
|
# NUMERIC ← Addition(numeric, numeric) | Modulo(numeric, numeric) |
|
|
243
293
|
# Size(sized) | First/Last(numeric array). `Size` returns an
|
|
244
294
|
# Integer for a String or an Array receiver alike (`SizedType` —
|
|
245
295
|
# `size_of`, resolver.rb) — both sides generated here.
|
|
296
|
+
#
|
|
297
|
+
# @param depth [Integer] maximum recursive steps remaining
|
|
298
|
+
# @return [Array<String>] numeric expressions one construct deep: `Addition`,
|
|
299
|
+
# `Modulo`, `Size` (of a string or array), `First`/`Last` (of a numeric array)
|
|
246
300
|
def numeric_productions(depth)
|
|
247
301
|
sub = bounded(:numeric, depth - 1)
|
|
248
302
|
# `Modulo`'s own receiver (not its argument — that side already
|
|
@@ -290,6 +344,11 @@ module Hecks
|
|
|
290
344
|
# `StartsWith`/`EndsWith`, `BlockPredicate` — every one of them
|
|
291
345
|
# parsed via a suffix regex inside `Resolver.parse` itself, per
|
|
292
346
|
# this generator's own design report) are eligible here.
|
|
347
|
+
#
|
|
348
|
+
# @param depth [Integer] maximum recursive steps remaining
|
|
349
|
+
# @return [Array<String>] boolean-typed expressions safe to embed as a
|
|
350
|
+
# Resolver-level suffix receiver: literals/lookups, sign tests, string
|
|
351
|
+
# predicates, `.empty?`, and sampled block predicates
|
|
293
352
|
def resolver_boolean_leaves(depth)
|
|
294
353
|
leaves(:boolean) +
|
|
295
354
|
resolver_numeric_leaves(depth).flat_map { |n| ["#{n}.positive?", "#{n}.negative?", "#{n}.zero?"] } +
|
|
@@ -320,6 +379,10 @@ module Hecks
|
|
|
320
379
|
# `Resolver.parse`'s greedy `(.+)\.suffix\z` regexes correctly
|
|
321
380
|
# isolate them as a receiver regardless of what precedes them —
|
|
322
381
|
# only bare top-level `+` has this problem).
|
|
382
|
+
#
|
|
383
|
+
# @param depth [Integer] maximum recursive steps remaining
|
|
384
|
+
# @return [Array<String>] numeric-typed expressions safe to embed as a
|
|
385
|
+
# Resolver-level suffix receiver (never a bare top-level `Addition`)
|
|
323
386
|
def resolver_numeric_leaves(depth)
|
|
324
387
|
return leaves(:numeric) if depth <= 0
|
|
325
388
|
|
|
@@ -342,6 +405,10 @@ module Hecks
|
|
|
342
405
|
# can't-actually-produce-it" mistake this sublanguage's own
|
|
343
406
|
# `ArrayLiteral` bug (§1 of this generator's own design report) was
|
|
344
407
|
# found from, inverted.
|
|
408
|
+
#
|
|
409
|
+
# @param depth [Integer] maximum recursive steps remaining
|
|
410
|
+
# @return [Array<String>] string expressions one construct deep: `ToS` of a
|
|
411
|
+
# numeric, boolean, nil, or string operand, or `First`/`Last` of a string array
|
|
345
412
|
def string_productions(depth)
|
|
346
413
|
sample(resolver_numeric_leaves(depth - 1)).map { |n| "#{n}.to_s" } +
|
|
347
414
|
sample(resolver_boolean_leaves(depth - 1)).map { |b| "#{b}.to_s" } +
|
|
@@ -357,6 +424,10 @@ module Hecks
|
|
|
357
424
|
# /`.any?`/`.include?`/…), rarely a produced value; the two real
|
|
358
425
|
# producers are enough to exercise every array-typed consumer
|
|
359
426
|
# elsewhere in this file at least once via a non-leaf path.
|
|
427
|
+
#
|
|
428
|
+
# @param depth [Integer] maximum recursive steps remaining
|
|
429
|
+
# @return [Array<String>] array expressions one construct deep: a string
|
|
430
|
+
# `Split`, a numeric array literal, and a string array literal; `[]` at depth 0
|
|
360
431
|
def array_productions(depth)
|
|
361
432
|
return [] if depth <= 0
|
|
362
433
|
|
|
@@ -364,7 +435,16 @@ module Hecks
|
|
|
364
435
|
[numeric_array_literal(depth - 1), string_array_literal(depth - 1)]
|
|
365
436
|
end
|
|
366
437
|
|
|
438
|
+
# Builds a two-element numeric array literal at `depth`.
|
|
439
|
+
#
|
|
440
|
+
# @param depth [Integer] maximum recursive steps remaining for its own elements
|
|
441
|
+
# @return [String] a `"[a, b]"` numeric array literal source text
|
|
367
442
|
def numeric_array_literal(depth) = "[#{bounded(:numeric, depth).first(2).join(', ')}]"
|
|
443
|
+
|
|
444
|
+
# Builds a two-element string array literal at `depth`.
|
|
445
|
+
#
|
|
446
|
+
# @param depth [Integer] maximum recursive steps remaining for its own elements
|
|
447
|
+
# @return [String] a `"[a, b]"` string array literal source text
|
|
368
448
|
def string_array_literal(depth) = "[#{bounded(:string, depth).first(2).join(', ')}]"
|
|
369
449
|
|
|
370
450
|
# Arrays known (by construction, not merely by type) to hold numeric
|
|
@@ -375,6 +455,10 @@ module Hecks
|
|
|
375
455
|
# this (and `string_array_productions`, its twin) is how that extra
|
|
376
456
|
# bit of type information flows without inventing a second, richer
|
|
377
457
|
# AST just to carry it.
|
|
458
|
+
#
|
|
459
|
+
# @param depth [Integer] maximum recursive steps remaining
|
|
460
|
+
# @return [Array<String>] array expressions known to hold numeric elements:
|
|
461
|
+
# `"arr_num"`, plus a numeric array literal at `depth`
|
|
378
462
|
def numeric_array_productions(depth)
|
|
379
463
|
["arr_num", numeric_array_literal(depth)]
|
|
380
464
|
# a Split of a string never yields numeric elements — no third
|
|
@@ -382,6 +466,12 @@ module Hecks
|
|
|
382
466
|
# `string_array_productions`, its non-empty twin, right below).
|
|
383
467
|
end
|
|
384
468
|
|
|
469
|
+
# Arrays known (by construction) to hold string elements — `arr_str`'s
|
|
470
|
+
# own twin of `numeric_array_productions`, above.
|
|
471
|
+
#
|
|
472
|
+
# @param depth [Integer] maximum recursive steps remaining
|
|
473
|
+
# @return [Array<String>] `"arr_str"`, a string array literal at `depth`, and
|
|
474
|
+
# every string `Split` expression at `depth`
|
|
385
475
|
def string_array_productions(depth)
|
|
386
476
|
["arr_str", string_array_literal(depth)] + bounded(:string, depth).map { |s| "#{s}.split(\",\")" }
|
|
387
477
|
end
|
|
@@ -401,6 +491,12 @@ module Hecks
|
|
|
401
491
|
# need `sub`/`num`/`str` threaded into each as parameters and
|
|
402
492
|
# would separate every rule from the comment justifying it.
|
|
403
493
|
# rubocop:disable-next Metrics/AbcSize
|
|
494
|
+
#
|
|
495
|
+
# @param depth [Integer] maximum recursive steps remaining
|
|
496
|
+
# @return [Array<String>] every boolean-typed construct in the grammar, one
|
|
497
|
+
# level deep: comparisons, sign tests, `.empty?`/`.match?`/`.present?`/
|
|
498
|
+
# `.blank?`/`.start_with?`/`.end_with?`, `&&`/`||`/`!`, block predicates,
|
|
499
|
+
# and `include?`
|
|
404
500
|
def boolean_productions(depth)
|
|
405
501
|
sub = bounded(:boolean, depth - 1)
|
|
406
502
|
num = bounded(:numeric, depth - 1)
|
|
@@ -444,6 +540,11 @@ module Hecks
|
|
|
444
540
|
# corpus already does this two levels deep —
|
|
445
541
|
# `examples/roster/bluebook/roster.bluebook`'s own `seats.any? { |s|
|
|
446
542
|
# assignments.none? { |a| … } }`).
|
|
543
|
+
#
|
|
544
|
+
# @param depth [Integer] maximum recursive steps remaining for the predicate body
|
|
545
|
+
# @return [Array<String>] `.all?`/`.any?`/`.none?` calls over each
|
|
546
|
+
# known-element-typed array source, one per predicate body `predicate_bodies`
|
|
547
|
+
# produces; `[]` if `depth` is negative
|
|
447
548
|
def block_predicate_productions(depth)
|
|
448
549
|
return [] if depth.negative?
|
|
449
550
|
|
|
@@ -467,6 +568,11 @@ module Hecks
|
|
|
467
568
|
# feeds three more constructs per body (`all?`/`any?`/`none?`) times
|
|
468
569
|
# four array sources, so an unbounded body list here is exactly the
|
|
469
570
|
# kind of multiplier this file's own `bounded` comment warns about.
|
|
571
|
+
#
|
|
572
|
+
# @param element_type [Symbol] the type `BLOCK_PARAM` is bound to inside the block
|
|
573
|
+
# @param depth [Integer] maximum recursive steps remaining for the body
|
|
574
|
+
# @return [Array<String>] a sampled set of boolean expressions, with
|
|
575
|
+
# `BLOCK_PARAM` admitted as a leaf of `element_type`
|
|
470
576
|
def predicate_bodies(element_type, depth)
|
|
471
577
|
sample(with_element_leaf(element_type) { boolean_productions(depth) })
|
|
472
578
|
end
|
|
@@ -476,6 +582,11 @@ module Hecks
|
|
|
476
582
|
# haystack admits any needle type (compared via `equal?`, itself
|
|
477
583
|
# numeric-coerced-first). Both sides generated here, matching
|
|
478
584
|
# `Vocabulary::IncludeHaystack` exactly.
|
|
585
|
+
#
|
|
586
|
+
# @param depth [Integer] maximum recursive steps remaining
|
|
587
|
+
# @return [Array<String>] `haystack.include?(needle)` expressions: a String
|
|
588
|
+
# haystack with a String needle, or a numeric/string array haystack with a
|
|
589
|
+
# matching-type needle
|
|
479
590
|
def include_productions(depth)
|
|
480
591
|
str = bounded(:string, depth)
|
|
481
592
|
pairs(str).map { |haystack, needle| "#{haystack}.include?(#{needle})" } +
|
|
@@ -507,8 +618,18 @@ module Hecks
|
|
|
507
618
|
# to enumerate first) rather than every possible pairing of them.
|
|
508
619
|
SAMPLE_CAP = 14
|
|
509
620
|
|
|
621
|
+
# Evenly samples `list` down to at most `SAMPLE_CAP` entries, so a list
|
|
622
|
+
# size never compounds unbounded across recursive passes.
|
|
623
|
+
#
|
|
624
|
+
# @param list [Array<String>] the list to sample
|
|
625
|
+
# @return [Array<String>] `list` unchanged if it is at most `SAMPLE_CAP` entries;
|
|
626
|
+
# otherwise every `list.size.fdiv(SAMPLE_CAP).ceil`-th entry, evenly spaced
|
|
510
627
|
def sample(list) = list.size <= SAMPLE_CAP ? list : list.each_slice(list.size.fdiv(SAMPLE_CAP).ceil).map(&:first)
|
|
511
628
|
|
|
629
|
+
# Builds every pair of one sampled list with itself.
|
|
630
|
+
#
|
|
631
|
+
# @param list [Array<String>] the list to sample and pair with itself
|
|
632
|
+
# @return [Array<Array(String, String)>] the sampled list's own self cross product
|
|
512
633
|
def pairs(list)
|
|
513
634
|
sampled = sample(list)
|
|
514
635
|
sampled.product(sampled)
|
|
@@ -518,6 +639,10 @@ module Hecks
|
|
|
518
639
|
# and right of a construct have genuinely different safety
|
|
519
640
|
# requirements (`Modulo`'s own receiver vs. argument, below) and
|
|
520
641
|
# squaring the same sampled list wouldn't be correct.
|
|
642
|
+
#
|
|
643
|
+
# @param left [Array<String>] the left-hand list to sample
|
|
644
|
+
# @param right [Array<String>] the right-hand list to sample
|
|
645
|
+
# @return [Array<Array(String, String)>] the two sampled lists' own cross product
|
|
521
646
|
def cross(left, right) = sample(left).product(sample(right))
|
|
522
647
|
|
|
523
648
|
# The full set — every boolean-typed expression up to `MAX_DEPTH`,
|
|
@@ -525,6 +650,10 @@ module Hecks
|
|
|
525
650
|
# wrapped, at every deeper level — `.uniq` inside `productions`
|
|
526
651
|
# already collapses most of that; this is the final pass over the
|
|
527
652
|
# complete depth-`MAX_DEPTH` set specifically).
|
|
653
|
+
#
|
|
654
|
+
# @param max_depth [Integer] maximum recursive depth to generate up to
|
|
655
|
+
# @return [Array<String>] every boolean-typed expression up to `max_depth`,
|
|
656
|
+
# deduplicated
|
|
528
657
|
def all_predicates(max_depth = MAX_DEPTH)
|
|
529
658
|
productions(:boolean, max_depth).uniq
|
|
530
659
|
end
|
|
@@ -536,6 +665,12 @@ module Hecks
|
|
|
536
665
|
# correctly, never a finding) and `{ok: false, error: ...}` only
|
|
537
666
|
# for anything else escaping — the one shape this whole file exists
|
|
538
667
|
# to prove never happens for well-typed input.
|
|
668
|
+
#
|
|
669
|
+
# @param expr [String] a generated (or hand-written) expression source string
|
|
670
|
+
# @return [Hash{Symbol => Object}] `{ok: true, result: Object}` for a real
|
|
671
|
+
# answer, `{ok: true, result: :refused, message: String}` for a clean
|
|
672
|
+
# `EvaluationError`, or `{ok: false, error: StandardError}` for anything else
|
|
673
|
+
# escaping
|
|
539
674
|
def check(expr)
|
|
540
675
|
result = Hecks::Bluebook::Expression::Evaluator.call(expr, synthetic_state, synthetic_attrs)
|
|
541
676
|
{ ok: true, result: result }
|
|
@@ -35,6 +35,10 @@ module Hecks
|
|
|
35
35
|
# Every adversarial domain plus every example — the corpus the
|
|
36
36
|
# census is measured over. Promoted generated domains are included:
|
|
37
37
|
# their shapes are already swept too.
|
|
38
|
+
#
|
|
39
|
+
# @param root [String] repository root to search under
|
|
40
|
+
# @return [Array<String>] paths of every stress/example corpus member that
|
|
41
|
+
# declares at least one bluebook file
|
|
38
42
|
def corpus_paths(root)
|
|
39
43
|
Hecks::Corpus.members(:stress, :example, root: root).map(&:path)
|
|
40
44
|
.select { |path| Hecks::Corpus.bluebook_files(path) }
|
|
@@ -44,6 +48,11 @@ module Hecks
|
|
|
44
48
|
# The last `limit` bug commit subjects on this branch — cheap, needs
|
|
45
49
|
# no ledger or Postgres, and names the mechanism in the title by
|
|
46
50
|
# convention (`BUG#n: …`).
|
|
51
|
+
#
|
|
52
|
+
# @param root [String] repository root to run `git log` in
|
|
53
|
+
# @param limit [Integer] maximum number of commit subjects to return
|
|
54
|
+
# @return [Array<String>] matching commit subjects, newest first; empty if
|
|
55
|
+
# `git log` fails
|
|
47
56
|
def recent_bug_titles(root, limit: 60)
|
|
48
57
|
out, status = Open3.capture2("git", "log", "--format=%s", "--grep=BUG#", "-n", limit.to_s, chdir: root)
|
|
49
58
|
status.success? ? out.lines.map(&:strip).reject(&:empty?) : []
|
|
@@ -51,6 +60,20 @@ module Hecks
|
|
|
51
60
|
|
|
52
61
|
# The mechanical half: which form pairs the corpus meets (and by
|
|
53
62
|
# whom), which it never meets, and what could not be measured.
|
|
63
|
+
#
|
|
64
|
+
# @param paths [Array<String>] corpus member paths to census, as returned by
|
|
65
|
+
# `#corpus_paths`
|
|
66
|
+
# @param root [String] repository root, to render `paths` relative to it
|
|
67
|
+
# @param bug_titles [Array<String>] recent bug commit subjects, as returned
|
|
68
|
+
# by `#recent_bug_titles`
|
|
69
|
+
# @return [Hash] `"forms"` (`Array<String>`, every declared form name),
|
|
70
|
+
# `"corpus"` (`Array<String>`, `paths` relative to `root`),
|
|
71
|
+
# `"unmet_pairs"` (`Array<String>`, form pairs no corpus member meets),
|
|
72
|
+
# `"single_carrier_pairs"` (`Array<String>`, pairs only one form name
|
|
73
|
+
# meets), `"skipped"` (`Array<String>`, paths that could not be
|
|
74
|
+
# censused, with the error), `"recent_bugs"` (`bug_titles`), and
|
|
75
|
+
# `"covered"` (`Hash{String => Array<String>}`, pair to every form name
|
|
76
|
+
# that meets it)
|
|
54
77
|
def brief(paths, root:, bug_titles:)
|
|
55
78
|
covered = Hash.new { |hash, key| hash[key] = [] }
|
|
56
79
|
skipped = []
|
|
@@ -66,6 +89,16 @@ module Hecks
|
|
|
66
89
|
"skipped" => skipped, "recent_bugs" => bug_titles, "covered" => covered }
|
|
67
90
|
end
|
|
68
91
|
|
|
92
|
+
# Fills `PROMPT_TEMPLATE`'s `{{placeholders}}` with `brief`'s own data, for
|
|
93
|
+
# the agent to answer by writing candidate bluebooks.
|
|
94
|
+
#
|
|
95
|
+
# @param root [String] repository root the prompt template is read from
|
|
96
|
+
# @param brief [Hash] the mechanical brief, as returned by `#brief`
|
|
97
|
+
# @param count [Integer] how many candidates the prompt asks the agent for
|
|
98
|
+
# @param out_dir [String] where the prompt asks the agent to write candidates
|
|
99
|
+
# @return [String] the filled-in prompt text
|
|
100
|
+
# @raise [KeyError] if the template names a placeholder `substitutions` does
|
|
101
|
+
# not provide
|
|
69
102
|
def prompt(root, brief, count:, out_dir:)
|
|
70
103
|
substitutions = {
|
|
71
104
|
"count" => count.to_s, "out_dir" => out_dir, "forms" => brief["forms"].join(", "),
|
|
@@ -78,6 +111,11 @@ module Hecks
|
|
|
78
111
|
|
|
79
112
|
# The second, narrower ask: these candidates did not boot, here is
|
|
80
113
|
# why, fix them in place. Nothing else in the directory changes.
|
|
114
|
+
#
|
|
115
|
+
# @param failures [Array<Array(Hash, String)>] `[candidate, error]` pairs,
|
|
116
|
+
# `candidate` as returned by `#candidates`, `error` the boot failure message
|
|
117
|
+
# @param out_dir [String] the directory the candidates were written under
|
|
118
|
+
# @return [String] the repair round's own prompt text
|
|
81
119
|
def repair_prompt(failures, out_dir:)
|
|
82
120
|
listed = failures.map { |candidate, error| "- #{candidate[:bluebook]}\n boot error: #{error}" }.join("\n")
|
|
83
121
|
<<~PROMPT
|
|
@@ -93,6 +131,11 @@ module Hecks
|
|
|
93
131
|
# `<out_dir>/<slug>/<anything>.bluebook` plus an optional
|
|
94
132
|
# HYPOTHESIS.md beside it. One bluebook per candidate: the first,
|
|
95
133
|
# alphabetically, if an agent wrote more.
|
|
134
|
+
#
|
|
135
|
+
# @param out_dir [String] the directory candidates were written under
|
|
136
|
+
# @return [Array<Hash>] one `{slug:, dir:, bluebook:, hypothesis:}` entry per
|
|
137
|
+
# candidate subdirectory that declares a `.bluebook` file; `hypothesis` is
|
|
138
|
+
# `nil` when no `HYPOTHESIS.md` sits beside it
|
|
96
139
|
def candidates(out_dir)
|
|
97
140
|
Dir[File.join(out_dir, "*")].select { |dir| File.directory?(dir) }.sort.filter_map do |dir|
|
|
98
141
|
bluebook = Dir[File.join(dir, "*.bluebook")].min
|
|
@@ -107,12 +150,28 @@ module Hecks
|
|
|
107
150
|
# Pairs the candidate meets on one aggregate that no corpus domain
|
|
108
151
|
# does — informational, never a gate: an agent may be aiming at a
|
|
109
152
|
# shape the census does not name yet.
|
|
153
|
+
#
|
|
154
|
+
# @param candidate_dir [String] path to the candidate's own directory
|
|
155
|
+
# @param covered [Hash{String => Array<String>}] the corpus's own covered
|
|
156
|
+
# pairs, as returned in `#brief`'s `"covered"` entry
|
|
157
|
+
# @return [Array<String>] form pairs the candidate meets that `covered` does
|
|
158
|
+
# not already name, sorted
|
|
110
159
|
def new_pairs(candidate_dir, covered)
|
|
111
160
|
FormCensus.covered_pairs(FormCensus.census(candidate_dir)).keys.reject { |pair| covered.key?(pair) }.sort
|
|
112
161
|
end
|
|
113
162
|
|
|
163
|
+
# Renders a path relative to the repository root.
|
|
164
|
+
#
|
|
165
|
+
# @param path [String] an absolute (or `root`-prefixed) path
|
|
166
|
+
# @param root [String] the prefix to strip
|
|
167
|
+
# @return [String] `path` with `root`'s own prefix removed
|
|
114
168
|
def relative(path, root) = path.delete_prefix("#{root}/")
|
|
115
169
|
|
|
170
|
+
# Renders a list of strings as a Markdown bullet list.
|
|
171
|
+
#
|
|
172
|
+
# @param items [Array<String>] lines to render as a Markdown bullet list
|
|
173
|
+
# @return [String] one `"- item"` line per entry, newline-joined; `"(none)"`
|
|
174
|
+
# when `items` is empty
|
|
116
175
|
def bulleted(items) = items.empty? ? "(none)" : items.map { |item| "- #{item}" }.join("\n")
|
|
117
176
|
end
|
|
118
177
|
end
|