hecks 1.4.0 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/hecks/adapters/driven/claude_code.rb +65 -0
- data/lib/hecks/adapters/driven/folder.rb +73 -0
- data/lib/hecks/adapters/driven/google_authentication.rb +25 -4
- data/lib/hecks/adapters/driven/governance_authorization.rb +57 -6
- data/lib/hecks/adapters/driven/heki/journal.rb +8 -0
- data/lib/hecks/adapters/driven/heki/saga_store.rb +53 -7
- data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
- data/lib/hecks/adapters/driven/heki.rb +93 -2
- data/lib/hecks/adapters/driven/identity_registry.rb +10 -0
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +22 -0
- data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
- data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
- data/lib/hecks/adapters/driven/lambda/client.rb +35 -7
- data/lib/hecks/adapters/driven/lambda.rb +43 -2
- data/lib/hecks/adapters/driven/local_storage.rb +67 -1
- data/lib/hecks/adapters/driven/memory.rb +13 -13
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +20 -0
- data/lib/hecks/adapters/driven/prism.rb +46 -7
- data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
- data/lib/hecks/adapters/driven/system_clock.rb +3 -0
- data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
- data/lib/hecks/adapters/driven.rb +2 -0
- data/lib/hecks/adapters/driving/github_webhook.rb +14 -1
- data/lib/hecks/behaviors/dsl.rb +58 -0
- data/lib/hecks/behaviors/expectations.rb +168 -7
- data/lib/hecks/behaviors/ir.rb +11 -0
- data/lib/hecks/behaviors/rspec.rb +8 -0
- data/lib/hecks/behaviors/runner.rb +19 -0
- data/lib/hecks/behaviors.rb +8 -0
- data/lib/hecks/bluebook/aggregate.rb +30 -0
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +7 -0
- data/lib/hecks/bluebook/assembly/build.rb +25 -4
- data/lib/hecks/bluebook/assembly/contract.rb +66 -14
- data/lib/hecks/bluebook/assembly/contracts.rb +24 -19
- data/lib/hecks/bluebook/assembly/marks.rb +143 -14
- data/lib/hecks/bluebook/assembly/specializer.rb +19 -0
- data/lib/hecks/bluebook/assembly.rb +25 -7
- data/lib/hecks/bluebook/attribute.rb +17 -3
- data/lib/hecks/bluebook/behaviour/aggregate.rb +32 -0
- data/lib/hecks/bluebook/behaviour/attribute.rb +13 -0
- data/lib/hecks/bluebook/behaviour/chapter.rb +55 -2
- data/lib/hecks/bluebook/behaviour/command.rb +33 -3
- data/lib/hecks/bluebook/behaviour/domain_port.rb +20 -0
- data/lib/hecks/bluebook/behaviour/entity.rb +12 -0
- data/lib/hecks/bluebook/behaviour/hexagon.rb +26 -0
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +23 -2
- data/lib/hecks/bluebook/behaviour/policy.rb +34 -9
- data/lib/hecks/bluebook/behaviour/process_manager.rb +32 -1
- data/lib/hecks/bluebook/behaviour/query.rb +5 -0
- data/lib/hecks/bluebook/behaviour/read_model.rb +21 -0
- data/lib/hecks/bluebook/behaviour/traits.rb +36 -0
- data/lib/hecks/bluebook/behaviour/value_object.rb +17 -5
- data/lib/hecks/bluebook/chapter.rb +23 -0
- data/lib/hecks/bluebook/command.rb +53 -8
- data/lib/hecks/bluebook/domain_port.rb +25 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +196 -37
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +79 -42
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +59 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +385 -53
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +135 -21
- data/lib/hecks/bluebook/dsl/command_builder.rb +189 -27
- data/lib/hecks/bluebook/dsl/entity_builder.rb +139 -9
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +41 -0
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +23 -6
- data/lib/hecks/bluebook/dsl/policy_builder.rb +60 -19
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +79 -15
- data/lib/hecks/bluebook/dsl/query_builder.rb +33 -4
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +102 -27
- data/lib/hecks/bluebook/dsl/rule_reference.rb +60 -8
- data/lib/hecks/bluebook/dsl/translation_builder.rb +146 -40
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +55 -7
- data/lib/hecks/bluebook/entity.rb +29 -0
- data/lib/hecks/bluebook/expression/ast_json.rb +115 -23
- data/lib/hecks/bluebook/expression/ast_reader.rb +29 -0
- data/lib/hecks/bluebook/expression/canonical_form.rb +48 -9
- data/lib/hecks/bluebook/expression/evaluator.rb +207 -29
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +36 -0
- data/lib/hecks/bluebook/expression/resolver.rb +320 -77
- data/lib/hecks/bluebook/hexagon.rb +34 -0
- data/lib/hecks/bluebook/lifecycle.rb +11 -0
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -0
- data/lib/hecks/bluebook/meta_validator/judge.rb +30 -26
- data/lib/hecks/bluebook/meta_validator/plan.rb +43 -8
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +1 -0
- data/lib/hecks/bluebook/meta_validator/readings.rb +157 -7
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +24 -4
- data/lib/hecks/bluebook/meta_validator/shapes.rb +141 -0
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +149 -23
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +5 -4
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +1 -0
- data/lib/hecks/bluebook/meta_validator.rb +180 -84
- data/lib/hecks/bluebook/model_check.rb +268 -24
- data/lib/hecks/bluebook/pattern_subset.rb +23 -1
- data/lib/hecks/bluebook/process_manager.rb +13 -0
- data/lib/hecks/bluebook/project_discovery.rb +5 -0
- data/lib/hecks/bluebook/project_loader.rb +40 -0
- data/lib/hecks/bluebook/project_register.rb +44 -0
- data/lib/hecks/bluebook/query.rb +27 -0
- data/lib/hecks/bluebook/read_model.rb +21 -1
- data/lib/hecks/bluebook/reference.rb +21 -8
- data/lib/hecks/bluebook/smoke_test.rb +29 -6
- data/lib/hecks/bluebook/synthesizer.rb +34 -0
- data/lib/hecks/bluebook/translation.rb +30 -1
- data/lib/hecks/bluebook/value_object.rb +23 -5
- data/lib/hecks/bluebook.rb +3 -4
- data/lib/hecks/codemod.rb +107 -20
- data/lib/hecks/construct.rb +15 -1
- data/lib/hecks/corpus.rb +146 -25
- data/lib/hecks/deploy/bluebook/deploy.bluebook +105 -0
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
- data/lib/hecks/deploy/oidc.json +5 -0
- data/lib/hecks/doc/reference.rb +185 -16
- data/lib/hecks/embryonaut_bluebook.rb +32 -9
- data/lib/hecks/facade/handle.rb +76 -3
- data/lib/hecks/facade/surface/aggregate_door.rb +8 -0
- data/lib/hecks/forms/field_shape.rb +3 -0
- data/lib/hecks/forms/page.rb +14 -0
- data/lib/hecks/forms/port_argument.rb +12 -0
- data/lib/hecks/forms/query_form_renderer.rb +63 -0
- data/lib/hecks/forms/record_renderer.rb +58 -0
- data/lib/hecks/forms/record_table.rb +27 -0
- data/lib/hecks/forms/reference_options.rb +24 -0
- data/lib/hecks/forms/value_object_shape.rb +10 -0
- data/lib/hecks/fqn.rb +58 -0
- data/lib/hecks/framework/bluebook/compliance.bluebook +221 -0
- data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
- data/lib/hecks/framework/oidc.json +15 -0
- data/lib/hecks/framework.rb +43 -20
- data/lib/hecks/freezer.rb +17 -1
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +159 -24
- data/lib/hecks/fuzzing/combination_miner.rb +59 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +109 -8
- data/lib/hecks/fuzzing/coverage_campaign.rb +56 -13
- data/lib/hecks/fuzzing/differential.rb +34 -0
- data/lib/hecks/fuzzing/domain_generator.rb +188 -11
- data/lib/hecks/fuzzing/era_boundary.rb +45 -15
- data/lib/hecks/fuzzing/form_census.rb +86 -0
- data/lib/hecks/fuzzing/generated_domain_check.rb +76 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +39 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +79 -22
- data/lib/hecks/fuzzing/nondeterministic.rb +13 -1
- data/lib/hecks/fuzzing/persistence_parity.rb +95 -3
- data/lib/hecks/fuzzing/properties/corrections.rb +25 -0
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +158 -14
- data/lib/hecks/fuzzing/properties/guards.rb +44 -0
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +48 -0
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +18 -0
- data/lib/hecks/fuzzing/properties/outbox.rb +49 -11
- data/lib/hecks/fuzzing/properties/querying.rb +68 -14
- data/lib/hecks/fuzzing/properties.rb +24 -15
- data/lib/hecks/fuzzing/qa_settings.rb +12 -0
- data/lib/hecks/fuzzing/replay.rb +137 -29
- data/lib/hecks/fuzzing/rotation_priority.rb +41 -21
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +46 -20
- data/lib/hecks/fuzzing/self_consistency.rb +189 -40
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +12 -6
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +1 -1
- data/lib/hecks/fuzzing/sequence_generator.rb +47 -0
- data/lib/hecks/fuzzing/shrinker.rb +114 -2
- data/lib/hecks/fuzzing/structural_skips.rb +18 -4
- data/lib/hecks/fuzzing/sweep_depth.rb +8 -0
- data/lib/hecks/fuzzing/target_capabilities.rb +61 -14
- data/lib/hecks/fuzzing/value_generator.rb +98 -10
- data/lib/hecks/grammar/evolve.rb +178 -2
- data/lib/hecks/grammar.rb +46 -0
- data/lib/hecks/ir.rb +38 -7
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
- data/lib/hecks/literal.rb +32 -0
- data/lib/hecks/naming.rb +88 -7
- data/lib/hecks/ports/access_control.rb +5 -10
- data/lib/hecks/ports/authorization.rb +3 -6
- data/lib/hecks/ports/identity_assignment.rb +1 -2
- data/lib/hecks/ports/identity_resolution.rb +1 -2
- data/lib/hecks/ports/key_vault.port +6 -0
- data/lib/hecks/ports/key_vault.rb +85 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +6 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +38 -2
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +40 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +92 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +8 -0
- data/lib/hecks/ports/query/in_memory.rb +39 -6
- data/lib/hecks/ports/query/ordering.rb +15 -0
- data/lib/hecks/ports.rb +1 -0
- data/lib/hecks/projections/bootstrap_table.rb +43 -8
- data/lib/hecks/projections/deploy/fargate.rb +666 -0
- data/lib/hecks/projections/deploy/lambda.rb +2423 -0
- data/lib/hecks/projections/deploy/shared.rb +624 -0
- data/lib/hecks/projections/diagrams.rb +243 -7
- data/lib/hecks/projections/glossary/html.rb +88 -0
- data/lib/hecks/projections/glossary/markdown.rb +47 -0
- data/lib/hecks/projections/glossary/mermaid.rb +48 -0
- data/lib/hecks/projections/glossary/sentences.rb +105 -0
- data/lib/hecks/projections/glossary.rb +161 -19
- data/lib/hecks/projections/model/deviations.rb +44 -0
- data/lib/hecks/projections/model.rb +51 -1
- data/lib/hecks/projections/oidc.rb +40 -3
- data/lib/hecks/projections/parser_table.rb +49 -8
- data/lib/hecks/projections/reference.rb +12 -0
- data/lib/hecks/projections/rust_vocabulary.rb +219 -16
- data/lib/hecks/projections/shape.rb +8 -0
- data/lib/hecks/projections/statements.rb +63 -16
- data/lib/hecks/projections/vocabulary.rb +17 -0
- data/lib/hecks/projections.rb +3 -0
- data/lib/hecks/projector/cli_projector.rb +218 -10
- data/lib/hecks/projector/docs_projector.rb +145 -19
- data/lib/hecks/projector/exporter.rb +65 -11
- data/lib/hecks/projector/ir_projector.rb +6 -0
- data/lib/hecks/projector/narrate_projector.rb +136 -15
- data/lib/hecks/projector/target.rb +47 -10
- data/lib/hecks/projector.rb +101 -19
- data/lib/hecks/query_ir.rb +47 -0
- data/lib/hecks/query_specification/common/null_policy.rb +5 -3
- data/lib/hecks/rendering.rb +6 -0
- data/lib/hecks/router/namespace_installer.rb +13 -0
- data/lib/hecks/router.rb +55 -0
- data/lib/hecks/runtime/aggregate_lock.rb +9 -0
- data/lib/hecks/runtime/boot_gates.rb +18 -0
- data/lib/hecks/runtime/caller.rb +32 -0
- data/lib/hecks/runtime/capability_graph.rb +11 -0
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +23 -21
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +14 -15
- data/lib/hecks/runtime/command_interpreter.rb +42 -17
- data/lib/hecks/runtime/command_rules/admissibility.rb +165 -14
- data/lib/hecks/runtime/command_rules/arithmetic.rb +17 -5
- data/lib/hecks/runtime/command_rules/references.rb +118 -28
- data/lib/hecks/runtime/dependency_planning.rb +45 -0
- data/lib/hecks/runtime/dispatcher.rb +28 -50
- data/lib/hecks/runtime/entity_element.rb +161 -8
- data/lib/hecks/runtime/entity_interpreter.rb +44 -9
- data/lib/hecks/runtime/errors.rb +18 -4
- data/lib/hecks/runtime/event.rb +10 -5
- data/lib/hecks/runtime/identity.rb +71 -3
- data/lib/hecks/runtime/instance.rb +67 -7
- data/lib/hecks/runtime/interpreting.rb +13 -5
- data/lib/hecks/runtime/invocation.rb +118 -36
- data/lib/hecks/runtime/loader.rb +94 -8
- data/lib/hecks/runtime/outbox.rb +145 -7
- data/lib/hecks/runtime/policy_interpreter.rb +22 -9
- data/lib/hecks/runtime/port_operation_interpreter.rb +20 -0
- data/lib/hecks/runtime/query_interpreter.rb +40 -12
- data/lib/hecks/runtime/reaction_invocation.rb +53 -8
- data/lib/hecks/runtime/read_model_interpreter.rb +23 -7
- data/lib/hecks/runtime/rebuild_sweep.rb +28 -0
- data/lib/hecks/runtime/reference_hop.rb +42 -0
- data/lib/hecks/runtime/refusal_wording.rb +50 -0
- data/lib/hecks/runtime/registry/saga_persistence.rb +11 -0
- data/lib/hecks/runtime/registry/verification.rb +119 -4
- data/lib/hecks/runtime/registry.rb +157 -4
- data/lib/hecks/runtime/remote_dispatcher.rb +92 -6
- data/lib/hecks/runtime/routing.rb +27 -2
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +10 -12
- data/lib/hecks/runtime/saga_interpreter.rb +27 -13
- data/lib/hecks/runtime/tenant_check.rb +26 -6
- data/lib/hecks/runtime/tenant_scope.rb +18 -0
- data/lib/hecks/runtime/value/coercion.rb +255 -33
- data/lib/hecks/runtime/value/entity_list_coercion.rb +102 -30
- data/lib/hecks/runtime/value.rb +50 -0
- data/lib/hecks/runtime.rb +32 -0
- data/lib/hecks/storehouse.rb +305 -9
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
- data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks.rb +79 -1
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -0
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +12 -2
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +29 -5
- metadata +14 -5
- data/lib/hecks/codemod/legacy_dispatch_args.rb +0 -299
- data/lib/hecks/codemod/legacy_dispatch_recorder.rb +0 -186
- data/lib/hecks/deprecation.rb +0 -95
- data/lib/hecks/framework/bluebook/compliance.bluebook +0 -1
|
@@ -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
|