hecks 2.7.0 → 2.8.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/exe/hecks +13 -0
- data/lib/hecks/adapters/driven/memory.rb +1 -1
- data/lib/hecks/adapters/driven/postgres.rb +1 -1
- data/lib/hecks/adapters/driven/prism.rb +1 -1
- data/lib/hecks/adapters/driven/sqlite/projection.rb +1 -1
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +31 -10
- data/lib/hecks/behaviors/expectations.rb +2 -2
- data/lib/hecks/bluebook/assembly/contract.rb +1 -1
- data/lib/hecks/bluebook/assembly/contracts.rb +5 -5
- data/lib/hecks/bluebook/assembly/specializer.rb +2 -2
- data/lib/hecks/bluebook/assembly.rb +1 -1
- data/lib/hecks/bluebook/behaviour/chapter.rb +5 -2
- data/lib/hecks/bluebook/behaviour/read_model.rb +18 -0
- data/lib/hecks/bluebook/capabilities.rb +2 -2
- data/lib/hecks/bluebook/chapter.rb +1 -1
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +2 -2
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +6 -1
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +2 -2
- data/lib/hecks/bluebook/dsl/entity_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +1 -1
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +14 -9
- data/lib/hecks/bluebook/dsl/rule_reference.rb +3 -3
- data/lib/hecks/bluebook/dsl/translation_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/world_builder.rb +5 -0
- data/lib/hecks/bluebook/meta_validator/judge.rb +3 -3
- data/lib/hecks/bluebook/meta_validator/plan.rb +4 -4
- data/lib/hecks/bluebook/meta_validator/readings.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +2 -2
- data/lib/hecks/bluebook/meta_validator/shapes.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +11 -10
- data/lib/hecks/bluebook/meta_validator.rb +1 -1
- data/lib/hecks/bluebook/model_check/client_profile.rb +4 -70
- data/lib/hecks/bluebook/model_check.rb +12 -13
- data/lib/hecks/cache_dir.rb +103 -0
- data/lib/hecks/cli/document.rb +104 -0
- data/lib/hecks/cli/ir.rb +47 -0
- data/lib/hecks/cli/mcp.rb +32 -0
- data/lib/hecks/cli/mcp_door.rb +461 -0
- data/lib/hecks/cli/model_check.rb +270 -0
- data/lib/hecks/cli/project_cli.rb +123 -0
- data/lib/hecks/cli/project_diagrams.rb +43 -0
- data/lib/hecks/cli/run.rb +219 -0
- data/lib/hecks/cli/smoke_test.rb +59 -0
- data/lib/hecks/cli/stores.rb +54 -0
- data/lib/hecks/cli.rb +166 -0
- data/lib/hecks/facade/handle.rb +2 -2
- data/lib/hecks/facade/surface/aggregate_door.rb +1 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +16 -0
- data/lib/hecks/mcp_door_scope.rb +176 -0
- data/lib/hecks/ports/persistence/append_only.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +12 -6
- data/lib/hecks/ports/persistence/state_codec.rb +1 -1
- data/lib/hecks/projections/deploy/fargate.rb +1 -1
- data/lib/hecks/projections/deploy/lambda.rb +3 -3
- data/lib/hecks/projections/reference.rb +7 -1
- data/lib/hecks/projector/exporter.rb +4 -4
- data/lib/hecks/projector/target.rb +1 -1
- data/lib/hecks/release/runner/ci_publisher.rb +96 -0
- data/lib/hecks/release/runner/commands.rb +68 -0
- data/lib/hecks/release/runner/console.rb +52 -0
- data/lib/hecks/release/runner/gem_publisher.rb +44 -0
- data/lib/hecks/release/runner/git.rb +48 -0
- data/lib/hecks/release/runner/npm_publisher.rb +89 -0
- data/lib/hecks/release/runner/preflight.rb +119 -0
- data/lib/hecks/release/runner/published.rb +55 -0
- data/lib/hecks/release/runner/tagger.rb +87 -0
- data/lib/hecks/release/runner.rb +244 -0
- data/lib/hecks/router/namespace_installer.rb +1 -1
- data/lib/hecks/runtime/command_interpreter.rb +3 -3
- data/lib/hecks/runtime/command_rules/admissibility.rb +2 -2
- data/lib/hecks/runtime/entity_element.rb +1 -1
- data/lib/hecks/runtime/entity_interpreter.rb +1 -1
- data/lib/hecks/runtime/invocation.rb +1 -1
- data/lib/hecks/runtime/loader.rb +1 -1
- data/lib/hecks/runtime/outbox.rb +1 -1
- data/lib/hecks/runtime/reaction_invocation.rb +3 -3
- data/lib/hecks/runtime/read_model_interpreter.rb +37 -15
- data/lib/hecks/runtime/reference_hop.rb +4 -4
- data/lib/hecks/runtime/registry/verification.rb +11 -11
- data/lib/hecks/runtime/registry.rb +8 -4
- data/lib/hecks/runtime/saga_interpreter.rb +1 -1
- data/lib/hecks/runtime/value/coercion.rb +3 -3
- data/lib/hecks/runtime/value/entity_list_coercion.rb +1 -1
- data/lib/hecks/storehouse.rb +18 -12
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +5 -0
- data/lib/hecks.rb +6 -1
- metadata +28 -58
- data/lib/hecks/bench/cli.rb +0 -74
- data/lib/hecks/bench/environment.rb +0 -100
- data/lib/hecks/bench/postgres_probe.rb +0 -41
- data/lib/hecks/bench/report.rb +0 -116
- data/lib/hecks/bench/ruby_runner.rb +0 -151
- data/lib/hecks/bench/run.rb +0 -55
- data/lib/hecks/bench/rust_runner.rb +0 -165
- data/lib/hecks/bench/stats.rb +0 -75
- data/lib/hecks/bench/suite.rb +0 -172
- data/lib/hecks/bench/workload.rb +0 -137
- data/lib/hecks/bench.rb +0 -28
- data/lib/hecks/codemod.rb +0 -443
- data/lib/hecks/corpus.rb +0 -479
- data/lib/hecks/doc/reference.rb +0 -585
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -684
- data/lib/hecks/fuzzing/combination_miner.rb +0 -178
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +0 -469
- data/lib/hecks/fuzzing/coverage_campaign.rb +0 -161
- data/lib/hecks/fuzzing/differential.rb +0 -192
- data/lib/hecks/fuzzing/domain_generator.rb +0 -871
- data/lib/hecks/fuzzing/era_boundary.rb +0 -154
- data/lib/hecks/fuzzing/form_census.rb +0 -285
- data/lib/hecks/fuzzing/generated_domain_check.rb +0 -171
- data/lib/hecks/fuzzing/invalid_value_generator.rb +0 -125
- data/lib/hecks/fuzzing/isolated_boot.rb +0 -658
- data/lib/hecks/fuzzing/nondeterministic.rb +0 -79
- data/lib/hecks/fuzzing/persistence_parity.rb +0 -253
- data/lib/hecks/fuzzing/properties/corrections.rb +0 -125
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +0 -688
- data/lib/hecks/fuzzing/properties/guards.rb +0 -398
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +0 -399
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +0 -137
- data/lib/hecks/fuzzing/properties/outbox.rb +0 -180
- data/lib/hecks/fuzzing/properties/querying.rb +0 -260
- data/lib/hecks/fuzzing/properties.rb +0 -267
- data/lib/hecks/fuzzing/qa_settings.rb +0 -164
- data/lib/hecks/fuzzing/replay.rb +0 -930
- data/lib/hecks/fuzzing/rotation_priority.rb +0 -114
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +0 -139
- data/lib/hecks/fuzzing/self_consistency.rb +0 -825
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +0 -532
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +0 -171
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +0 -127
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +0 -116
- data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +0 -138
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +0 -344
- data/lib/hecks/fuzzing/sequence_generator.rb +0 -311
- data/lib/hecks/fuzzing/shrinker.rb +0 -309
- data/lib/hecks/fuzzing/structural_skips.rb +0 -53
- data/lib/hecks/fuzzing/sweep_depth.rb +0 -61
- data/lib/hecks/fuzzing/target_capabilities.rb +0 -255
- data/lib/hecks/fuzzing/value_generator.rb +0 -340
- data/lib/hecks/fuzzing.rb +0 -23
- data/lib/hecks/grammar/evolve.rb +0 -524
- data/lib/hecks/query_ir.rb +0 -482
data/lib/hecks/codemod.rb
DELETED
|
@@ -1,443 +0,0 @@
|
|
|
1
|
-
require_relative "bluebook/meta_validator"
|
|
2
|
-
require_relative "corpus"
|
|
3
|
-
|
|
4
|
-
module Hecks
|
|
5
|
-
# Shared machinery for a codemod that migrates real `.bluebook` source.
|
|
6
|
-
#
|
|
7
|
-
# ## Why this module exists
|
|
8
|
-
#
|
|
9
|
-
# For migrating source once a DSL builder change turns a required
|
|
10
|
-
# declaration into an optional, redundant one — pulled out of
|
|
11
|
-
# `bin/codemod_implicit_append_fields` (the first one built), which
|
|
12
|
-
# needed three real, hard-won fixes before it could be trusted: a
|
|
13
|
-
# process-lifetime AST cache with no invalidation, a batch-revert
|
|
14
|
-
# granularity that let one unsafe candidate sink every other safe one
|
|
15
|
-
# sharing its boot, and (in the spine the codemod migrates for, not
|
|
16
|
-
# here) an append-at-end insertion that only round-tripped correctly
|
|
17
|
-
# for whichever field happened to be last. None of those are guessable
|
|
18
|
-
# in advance; they only surface by actually running a real edit
|
|
19
|
-
# against real self-hosted code. This module is that lesson, kept —
|
|
20
|
-
# the next codemod plugs in two rule-specific procs (`find_candidates`,
|
|
21
|
-
# `apply_candidate`) and inherits the boot/safety-net machinery rather
|
|
22
|
-
# than rediscovering it.
|
|
23
|
-
#
|
|
24
|
-
# ## The three-step contract
|
|
25
|
-
#
|
|
26
|
-
# **A codemod is not pattern-matching alone**. Deciding "is this line safe
|
|
27
|
-
# to delete" means knowing what the runtime would resolve it to — so
|
|
28
|
-
# every codemod built on this module follows the same three steps:
|
|
29
|
-
# 1. Boot the real domain (or the self-hosted meta-domain) and read
|
|
30
|
-
# its IR to find candidates — provided by the caller's own
|
|
31
|
-
# `find_candidates`, since the actual redundancy rule is specific
|
|
32
|
-
# to whichever spine change this migration serves.
|
|
33
|
-
# 2. Locate and remove each candidate's own source text — the
|
|
34
|
-
# caller's own `apply_candidate`.
|
|
35
|
-
# 3. Re-boot from the edited text and diff the full IR export
|
|
36
|
-
# against the pre-edit export. Byte-identical -> keep. Anything
|
|
37
|
-
# else (including a raised exception — the self-hosted
|
|
38
|
-
# meta-domain dispatches itself into being, S14, so a bad edit
|
|
39
|
-
# can surface as a runtime refusal, not just a differing export)
|
|
40
|
-
# -> revert and report skipped, never silently guessed past.
|
|
41
|
-
#
|
|
42
|
-
# ## Design-time checklist for a future codemod
|
|
43
|
-
#
|
|
44
|
-
# For the spine change a future codemod migrates corpus text for —
|
|
45
|
-
# both items below are real bugs this module's own first use found,
|
|
46
|
-
# not hypothetical:
|
|
47
|
-
# - Does the resolved value get inserted into an order-sensitive
|
|
48
|
-
# list (the exported IR is array-order-sensitive throughout)? If
|
|
49
|
-
# so, the spine's own insertion must preserve the original
|
|
50
|
-
# position, not just append — an append-at-end insertion only
|
|
51
|
-
# round-trips correctly for a value that already happened to be
|
|
52
|
-
# last.
|
|
53
|
-
# - Does resolution depend on another construct already being
|
|
54
|
-
# declared (the aggregate/entity a `sets`/`append:` field resolves
|
|
55
|
-
# against)? If a creator command can be declared before the
|
|
56
|
-
# construct it creates (real, live: `command "Handler"` before
|
|
57
|
-
# `entity "Handler"`, one file down), one-pass resolution
|
|
58
|
-
# genuinely cannot see it yet — not a codemod bug, a structural
|
|
59
|
-
# limit worth naming rather than working around.
|
|
60
|
-
module Codemod
|
|
61
|
-
ROOT = File.expand_path("../..", __dir__)
|
|
62
|
-
|
|
63
|
-
EXAMPLE_ROOTS = Corpus.members(:example, root: ROOT).map(&:path)
|
|
64
|
-
META_FILES = (Dir.glob(File.join(ROOT, "lib/hecks/grammar/*.bluebook")) +
|
|
65
|
-
Dir.glob(File.join(ROOT, "lib/hecks/framework/bluebook/*.bluebook")) +
|
|
66
|
-
Dir.glob(File.join(ROOT, "lib/hecks/language/bluebook/**/*.bluebook"))).sort
|
|
67
|
-
|
|
68
|
-
# The same lightweight path `spec/spec_helper.rb`'s own
|
|
69
|
-
# `boot_in_memory` uses — `Hecks.with_registry` satisfies
|
|
70
|
-
# `Hecks.bluebook`'s own `collect`'s "loaded outside a boot" check
|
|
71
|
-
# without `Hecks.boot`'s full era-check/adapter-wiring path, which
|
|
72
|
-
# needs a live Postgres connection for `compliance` and would make
|
|
73
|
-
# every codemod depend on a database it has no reason to touch — a
|
|
74
|
-
# codemod only ever reads a chapter's own declared IR, never a
|
|
75
|
-
# stored record.
|
|
76
|
-
PERSISTENCE_PORT = File.join(ROOT, "lib/hecks/ports/persistence.port")
|
|
77
|
-
EXTRACTION_PORT = File.join(ROOT, "lib/hecks/ports/extraction.port")
|
|
78
|
-
MEMORY_ADAPTER = File.join(ROOT, "lib/hecks/adapters/driven/memory.adapter")
|
|
79
|
-
PRISM_ADAPTER = File.join(ROOT, "lib/hecks/adapters/driven/prism.adapter")
|
|
80
|
-
|
|
81
|
-
# Exports a booted registry's canonical IR as JSON, for a before/after diff.
|
|
82
|
-
#
|
|
83
|
-
# @param registry [Runtime::Registry] the booted registry to export
|
|
84
|
-
# @return [String] the pretty-printed JSON IR export
|
|
85
|
-
def self.export_json(registry) = Hecks::Projector::Exporter.json(registry)
|
|
86
|
-
|
|
87
|
-
# `Hecks::Adapters::Prism` caches a file's parsed AST for the
|
|
88
|
-
# life of the process, keyed by path — fine for every existing
|
|
89
|
-
# caller (a file loads once per process: one `bin/ir` run, one
|
|
90
|
-
# rspec worker), but a codemod legitimately reloads the same path
|
|
91
|
-
# after editing it, and a stale cached tree reports a
|
|
92
|
-
# `given`/`ensures` block at its old line number, which no longer
|
|
93
|
-
# matches the freshly re-executed file's own `block.source_location`
|
|
94
|
-
# — surfacing as "did not survive extraction" on a perfectly valid
|
|
95
|
-
# file. `Prism.forget` is the real invalidation API this module's
|
|
96
|
-
# own first use motivated (found here, fixed at the source rather
|
|
97
|
-
# than left as a private `TREES.clear` poke from outside).
|
|
98
|
-
#
|
|
99
|
-
# @param path [String, Array<String>] a single `.bluebook` file, a directory
|
|
100
|
-
# to glob every `.bluebook` file from, or an explicit list of file paths
|
|
101
|
-
# @return [Runtime::Registry] a fresh registry with every file loaded and judged
|
|
102
|
-
def self.load_bluebook(path)
|
|
103
|
-
paths = if path.is_a?(Array)
|
|
104
|
-
path
|
|
105
|
-
elsif File.directory?(path)
|
|
106
|
-
Dir.glob(File.join(path, "*.bluebook"))
|
|
107
|
-
else
|
|
108
|
-
[path]
|
|
109
|
-
end
|
|
110
|
-
paths.each { |file| Hecks::Adapters::Prism.forget(file) }
|
|
111
|
-
|
|
112
|
-
registry = Hecks::Runtime::Registry.new
|
|
113
|
-
Hecks.with_registry(registry) do
|
|
114
|
-
Kernel.load(PERSISTENCE_PORT)
|
|
115
|
-
Kernel.load(EXTRACTION_PORT)
|
|
116
|
-
Kernel.load(MEMORY_ADAPTER)
|
|
117
|
-
Kernel.load(PRISM_ADAPTER)
|
|
118
|
-
Hecks::Bluebook::MetaValidator.defer { paths.each { |file| Kernel.load(file) } }
|
|
119
|
-
Hecks::Bluebook::MetaValidator.judge_deferred!(registry)
|
|
120
|
-
end
|
|
121
|
-
registry
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
# `forget_all`, not a single `forget` — the meta-domain is nine
|
|
125
|
-
# files (`MetaValidator::GRAMMAR_FILES`) merged into one registry,
|
|
126
|
-
# and a caller here (the codemod runner) doesn't generally know in
|
|
127
|
-
# advance which one it just edited.
|
|
128
|
-
# @return [String] the freshly re-derived meta-domain's canonical IR, as JSON
|
|
129
|
-
def self.boot_meta
|
|
130
|
-
Hecks::Adapters::Prism.forget_all
|
|
131
|
-
Hecks::Bluebook::MetaValidator.instance_variable_set(:@grammar_registry, nil)
|
|
132
|
-
export_json(Hecks::Bluebook::MetaValidator.grammar_registry)
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
# Forces the self-hosted meta-domain to re-derive from its current source,
|
|
136
|
-
# discarding any cached parse trees or memoized registry.
|
|
137
|
-
#
|
|
138
|
-
# @return [Runtime::Registry] the freshly re-derived meta-domain's registry
|
|
139
|
-
def self.meta_registry
|
|
140
|
-
Hecks::Adapters::Prism.forget_all
|
|
141
|
-
Hecks::Bluebook::MetaValidator.instance_variable_set(:@grammar_registry, nil)
|
|
142
|
-
Hecks::Bluebook::MetaValidator.grammar_registry
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
# Walks every aggregate (and every nested entity, recursively)
|
|
146
|
-
# across every chapter in a booted registry, yielding [owning
|
|
147
|
-
# construct, command] pairs — `construct` is whichever
|
|
148
|
-
# Aggregate/Entity actually owns the command, the same distinction
|
|
149
|
-
# `AggregateBuilder#command` vs `EntityBuilder#command` already
|
|
150
|
-
# draws. Generic enough for any rule that needs to walk real
|
|
151
|
-
# commands, not specific to the attribute-redundancy rule.
|
|
152
|
-
#
|
|
153
|
-
# @param registry [Runtime::Registry] a booted registry
|
|
154
|
-
# @yield [construct, command] every command in the registry, once per aggregate
|
|
155
|
-
# and once per nested entity
|
|
156
|
-
# @yieldparam construct [Bluebook::Aggregate, Bluebook::Entity] whichever
|
|
157
|
-
# aggregate or entity actually owns `command`
|
|
158
|
-
# @yieldparam command [Bluebook::Command] the command
|
|
159
|
-
# @return [Hash{String => Bluebook::Chapter}] `registry.bluebooks`, unchanged
|
|
160
|
-
def self.each_command(registry)
|
|
161
|
-
registry.bluebooks.each_value do |chapter|
|
|
162
|
-
chapter.aggregates.each do |aggregate|
|
|
163
|
-
walk = lambda do |construct|
|
|
164
|
-
construct.commands.each { |command| yield construct, command }
|
|
165
|
-
construct.entities.each(&walk) if construct.respond_to?(:entities)
|
|
166
|
-
end
|
|
167
|
-
walk.call(aggregate)
|
|
168
|
-
end
|
|
169
|
-
end
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
# Finds one construct's own attribute by name.
|
|
173
|
-
#
|
|
174
|
-
# @param construct [Bluebook::Aggregate, Bluebook::Entity] the construct to search
|
|
175
|
-
# @param name [String, Symbol, #to_s] the attribute's declared name
|
|
176
|
-
# @return [Bluebook::Attribute, nil] the matching attribute, or nil if `construct`
|
|
177
|
-
# declares no attribute named `name`
|
|
178
|
-
def self.owner_attribute(construct, name)
|
|
179
|
-
construct.attributes.find { |attr| attr.name.to_s == name.to_s }
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
# A list attribute's own element construct — the value object or
|
|
183
|
-
# entity `list_of(...)` names, resolved by `hecks_name` the same way
|
|
184
|
-
# `AttributeCollector#resolve_identity_field!` already does. Shared
|
|
185
|
-
# because "what does this list actually hold" is a question any
|
|
186
|
-
# append-shaped rule needs answered, not just this one.
|
|
187
|
-
#
|
|
188
|
-
# Nothing in the DSL stops a value object and an entity nested under
|
|
189
|
-
# the same aggregate from sharing a `hecks_name` — they are declared
|
|
190
|
-
# through two separate collections with no cross-kind uniqueness
|
|
191
|
-
# check between them. A silent value-objects-before-entities pick
|
|
192
|
-
# would resolve such a pair the same way every time regardless of
|
|
193
|
-
# which one `list_field` actually names, so a genuine collision is
|
|
194
|
-
# refused instead, the same "never first-wins silently" call
|
|
195
|
-
# `EntityBuilder#install_closed_sets!` already makes for a same-named
|
|
196
|
-
# closed set.
|
|
197
|
-
#
|
|
198
|
-
# @param construct [Bluebook::Aggregate, Bluebook::Entity] the construct
|
|
199
|
-
# declaring `list_field`
|
|
200
|
-
# @param list_field [String, Symbol, #to_s] the name of the `list_of(...)` attribute
|
|
201
|
-
# @return [Bluebook::Entity, Bluebook::ValueObject, nil] the construct
|
|
202
|
-
# `list_field` holds a list of, or nil if `list_field` names no attribute,
|
|
203
|
-
# isn't a list, or names no known value object or entity
|
|
204
|
-
# @raise [RuntimeError] if a value object and an entity both match `list_field`'s type
|
|
205
|
-
def self.element_construct_for(construct, list_field)
|
|
206
|
-
list_attr = owner_attribute(construct, list_field)
|
|
207
|
-
return nil unless list_attr&.list?
|
|
208
|
-
|
|
209
|
-
value_objects = construct.respond_to?(:value_objects) ? construct.value_objects : []
|
|
210
|
-
entities = construct.respond_to?(:entities) ? construct.entities : []
|
|
211
|
-
matches = (value_objects + entities).select { |c| c.hecks_name.to_s == list_attr.type.to_s }
|
|
212
|
-
|
|
213
|
-
if matches.size > 1
|
|
214
|
-
raise "#{construct.hecks_name}##{list_field} names #{list_attr.type}, held by both a value " \
|
|
215
|
-
"object and an entity — ambiguous, cannot resolve which one the list holds"
|
|
216
|
-
end
|
|
217
|
-
|
|
218
|
-
matches.first
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
# Either a raised exception or a differing export counts as unsafe
|
|
222
|
-
# — see the module header on why the meta-domain specifically can
|
|
223
|
-
# raise. Returns [value_or_nil, error_message_or_nil].
|
|
224
|
-
#
|
|
225
|
-
# @yield the risky boot/export step to run
|
|
226
|
-
# @return [Array(Object, nil), Array(nil, String)] `[the block's result, nil]`
|
|
227
|
-
# on success, or `[nil, "ExceptionClass: message"]` if the block raises
|
|
228
|
-
# any StandardError
|
|
229
|
-
def self.safely
|
|
230
|
-
[yield, nil]
|
|
231
|
-
rescue StandardError => e
|
|
232
|
-
[nil, "#{e.class}: #{e.message}"]
|
|
233
|
-
end
|
|
234
|
-
|
|
235
|
-
# **The reusable runner** — every real bug fix this module carries lives
|
|
236
|
-
# here, not in a caller's own script. A caller supplies:
|
|
237
|
-
#
|
|
238
|
-
# find_candidates: ->(registry) { [...] }
|
|
239
|
-
# Given a booted registry, return every candidate this rule
|
|
240
|
-
# could migrate. A candidate is whatever shape the caller wants
|
|
241
|
-
# — `apply_candidate` is the only other thing that reads it.
|
|
242
|
-
#
|
|
243
|
-
# apply_candidate: ->(text, candidate) { [new_text, changed_bool] }
|
|
244
|
-
# Given one file's current text and one candidate, return the
|
|
245
|
-
# edited text and whether a match was actually found/removed —
|
|
246
|
-
# `false` (text unchanged) when the candidate doesn't apply to
|
|
247
|
-
# this file, which is how the meta-domain's multi-file search
|
|
248
|
-
# below finds the right one without the caller needing to know
|
|
249
|
-
# which file a candidate lives in ahead of time.
|
|
250
|
-
#
|
|
251
|
-
# label: ->(candidate) { "..." }
|
|
252
|
-
# One-line description for the results report.
|
|
253
|
-
class Runner
|
|
254
|
-
# @param find_candidates [Proc] `->(registry) { [...] }` — given a booted
|
|
255
|
-
# registry, returns every candidate this rule could migrate; a candidate's
|
|
256
|
-
# shape is the caller's own, read back only by `apply_candidate`
|
|
257
|
-
# @param apply_candidate [Proc] `->(text, candidate) { [new_text, changed_bool] }`
|
|
258
|
-
# — given one file's current text and one candidate, returns the edited text
|
|
259
|
-
# and whether a match was found; `false` means the candidate doesn't apply to
|
|
260
|
-
# this file
|
|
261
|
-
# @param label [Proc] `->(candidate) { "..." }` — a one-line description of a
|
|
262
|
-
# candidate, for the results report
|
|
263
|
-
def initialize(find_candidates:, apply_candidate:, label:)
|
|
264
|
-
@find_candidates = find_candidates
|
|
265
|
-
@apply_candidate = apply_candidate
|
|
266
|
-
@label = label
|
|
267
|
-
end
|
|
268
|
-
|
|
269
|
-
# Runs this rule's codemod across every example domain and the meta-domain.
|
|
270
|
-
#
|
|
271
|
-
# @param dry_run [Boolean] when true, every safe edit is still written and
|
|
272
|
-
# reverified, then reverted rather than kept
|
|
273
|
-
# @return [Hash{Symbol => Array}] `:applied` (`Array<Hash{file: String,
|
|
274
|
-
# candidates: Array<String>}>`), `:skipped` (`Array<Hash{file: String,
|
|
275
|
-
# reason: String, candidates: Array<String>}>`), and `:clean`
|
|
276
|
-
# (`Array<String>` of domain directories or `"meta-domain"` with no candidates)
|
|
277
|
-
def run(dry_run: false)
|
|
278
|
-
results = { applied: [], skipped: [], clean: [] }
|
|
279
|
-
run_example_domains(results, dry_run)
|
|
280
|
-
run_meta_domain(results, dry_run)
|
|
281
|
-
results
|
|
282
|
-
end
|
|
283
|
-
|
|
284
|
-
# Prints `run`'s results to stdout.
|
|
285
|
-
#
|
|
286
|
-
# @param results [Hash{Symbol => Array}] a `run` result
|
|
287
|
-
# @param dry_run [Boolean] whether this was a dry run, for the report's own heading
|
|
288
|
-
# @return [void]
|
|
289
|
-
def report(results, dry_run:)
|
|
290
|
-
puts "== results (#{dry_run ? 'DRY RUN — nothing written' : 'applied'}) =="
|
|
291
|
-
puts "clean (no candidates): #{results[:clean].join(', ')}" unless results[:clean].empty?
|
|
292
|
-
results[:applied].each { |r| puts "APPLIED #{r[:file]}: #{r[:candidates].join(', ')}" }
|
|
293
|
-
results[:skipped].each { |r| puts "SKIPPED #{r[:file]} (#{r[:reason]}): #{Array(r[:candidates]).join(', ')}" }
|
|
294
|
-
end
|
|
295
|
-
|
|
296
|
-
private
|
|
297
|
-
|
|
298
|
-
def apply_many(text, candidates)
|
|
299
|
-
applied = []
|
|
300
|
-
candidates.each do |c|
|
|
301
|
-
text, changed = @apply_candidate.call(text, c)
|
|
302
|
-
applied << c if changed
|
|
303
|
-
end
|
|
304
|
-
[text, applied]
|
|
305
|
-
end
|
|
306
|
-
|
|
307
|
-
# The per-candidate write/verify/revert sequence below is one
|
|
308
|
-
# coherent unit (see the comment ahead of the candidates.each
|
|
309
|
-
# loop for why it can't batch) — splitting it across methods just
|
|
310
|
-
# to satisfy the line count would scatter state four ways for no
|
|
311
|
-
# readability gain.
|
|
312
|
-
# rubocop:disable-next Metrics/AbcSize
|
|
313
|
-
# rubocop:disable-next Metrics/BlockLength
|
|
314
|
-
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
315
|
-
# rubocop:disable-next Metrics/MethodLength
|
|
316
|
-
# rubocop:disable-next Metrics/PerceivedComplexity
|
|
317
|
-
def run_example_domains(results, dry_run)
|
|
318
|
-
Codemod::EXAMPLE_ROOTS.each do |domain_dir|
|
|
319
|
-
bluebook_files = Dir.glob(File.join(domain_dir, "bluebook", "*.bluebook"))
|
|
320
|
-
next if bluebook_files.empty?
|
|
321
|
-
|
|
322
|
-
before_json = Codemod.export_json(Codemod.load_bluebook(bluebook_files))
|
|
323
|
-
candidates = @find_candidates.call(Codemod.load_bluebook(bluebook_files))
|
|
324
|
-
|
|
325
|
-
if candidates.empty?
|
|
326
|
-
results[:clean] << domain_dir
|
|
327
|
-
next
|
|
328
|
-
end
|
|
329
|
-
|
|
330
|
-
live = bluebook_files.to_h { |file| [file, File.read(file)] }
|
|
331
|
-
applied_by_file = Hash.new { |hash, file| hash[file] = [] }
|
|
332
|
-
|
|
333
|
-
candidates.each do |candidate|
|
|
334
|
-
target_file = bluebook_files.find { |file| apply_many(live[file], [candidate]).last.any? }
|
|
335
|
-
unless target_file
|
|
336
|
-
results[:skipped] << { file: domain_dir, reason: "no matching source line", candidates: [@label.call(candidate)] }
|
|
337
|
-
next
|
|
338
|
-
end
|
|
339
|
-
|
|
340
|
-
original_text = live[target_file]
|
|
341
|
-
text, = apply_many(original_text, [candidate])
|
|
342
|
-
live[target_file] = text
|
|
343
|
-
File.write(target_file, text)
|
|
344
|
-
after_json, error = Codemod.safely do
|
|
345
|
-
Codemod.export_json(Codemod.load_bluebook(bluebook_files))
|
|
346
|
-
end
|
|
347
|
-
|
|
348
|
-
if after_json == before_json && !dry_run
|
|
349
|
-
applied_by_file[target_file] << candidate
|
|
350
|
-
elsif after_json == before_json
|
|
351
|
-
live[target_file] = original_text
|
|
352
|
-
File.write(target_file, original_text)
|
|
353
|
-
Codemod.safely { Codemod.load_bluebook(bluebook_files) }
|
|
354
|
-
applied_by_file[target_file] << candidate
|
|
355
|
-
else
|
|
356
|
-
live[target_file] = original_text
|
|
357
|
-
File.write(target_file, original_text)
|
|
358
|
-
Codemod.safely { Codemod.load_bluebook(bluebook_files) }
|
|
359
|
-
reason = error ? "reboot raised after edit (#{error}) — reverted" : "IR changed after edit — reverted"
|
|
360
|
-
results[:skipped] << { file: target_file, reason: reason, candidates: [@label.call(candidate)] }
|
|
361
|
-
end
|
|
362
|
-
end
|
|
363
|
-
|
|
364
|
-
applied_by_file.each do |file, applied|
|
|
365
|
-
results[:applied] << { file: file, candidates: applied.map(&@label) }
|
|
366
|
-
end
|
|
367
|
-
end
|
|
368
|
-
end
|
|
369
|
-
|
|
370
|
-
# Per-candidate, not one batched write-then-verify — the
|
|
371
|
-
# meta-domain is one shared registry (SyntaxBoot dispatches it
|
|
372
|
-
# into itself, S14), so a single unsafe candidate among many would
|
|
373
|
-
# otherwise sink every other, genuinely safe candidate in the same
|
|
374
|
-
# run: this module's first real run found exactly that (25
|
|
375
|
-
# candidates, one dispatch-time break, all 25 reverted as a batch
|
|
376
|
-
# before this per-candidate loop existed). `before_meta` stays the
|
|
377
|
-
# fixed reference throughout — a truly safe candidate never
|
|
378
|
-
# changes the IR, so the running state's own export should always
|
|
379
|
-
# still equal the pristine original after each kept edit, by
|
|
380
|
-
# definition, no moving target needed.
|
|
381
|
-
# Same shape and same reason as run_example_domains just above (see
|
|
382
|
-
# its own comment) — the write/verify/revert sequence, per
|
|
383
|
-
# candidate (this method's own comment explains why it cannot
|
|
384
|
-
# batch), is one coherent unit; splitting it would scatter
|
|
385
|
-
# before_meta/live_meta/applied_by_file state across methods for
|
|
386
|
-
# no gain.
|
|
387
|
-
# rubocop:disable-next Metrics/AbcSize
|
|
388
|
-
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
389
|
-
# rubocop:disable-next Metrics/PerceivedComplexity
|
|
390
|
-
def run_meta_domain(results, dry_run)
|
|
391
|
-
before_meta = Codemod.boot_meta
|
|
392
|
-
meta_candidates = @find_candidates.call(Codemod.meta_registry)
|
|
393
|
-
|
|
394
|
-
if meta_candidates.empty?
|
|
395
|
-
results[:clean] << "meta-domain"
|
|
396
|
-
return
|
|
397
|
-
end
|
|
398
|
-
|
|
399
|
-
live_meta = Codemod::META_FILES.to_h { |f| [f, File.read(f)] }
|
|
400
|
-
applied_by_file = Hash.new { |h, k| h[k] = [] }
|
|
401
|
-
|
|
402
|
-
meta_candidates.each do |candidate|
|
|
403
|
-
target_file = Codemod::META_FILES.find { |f| apply_many(live_meta[f], [candidate]).last.any? }
|
|
404
|
-
unless target_file
|
|
405
|
-
results[:skipped] << { file: "meta-domain", reason: "no matching source line", candidates: [@label.call(candidate)] }
|
|
406
|
-
next
|
|
407
|
-
end
|
|
408
|
-
|
|
409
|
-
text, = apply_many(live_meta[target_file], [candidate])
|
|
410
|
-
original_text = live_meta[target_file]
|
|
411
|
-
live_meta[target_file] = text
|
|
412
|
-
Codemod::META_FILES.each { |f| File.write(f, live_meta[f]) }
|
|
413
|
-
|
|
414
|
-
# **Dry run still verifies** — see run_example_domains' own
|
|
415
|
-
# comment; the edit is always written and rebooted for real,
|
|
416
|
-
# then always reverted afterward when dry-run (whether or not
|
|
417
|
-
# it was safe) so the next candidate is judged against the
|
|
418
|
-
# true original state, never an accumulated hypothetical.
|
|
419
|
-
after_meta, error = Codemod.safely { Codemod.boot_meta }
|
|
420
|
-
|
|
421
|
-
if after_meta == before_meta && !dry_run
|
|
422
|
-
applied_by_file[target_file] << candidate
|
|
423
|
-
elsif after_meta == before_meta # dry-run, safe — revert, but count as applied
|
|
424
|
-
live_meta[target_file] = original_text
|
|
425
|
-
File.write(target_file, original_text)
|
|
426
|
-
Codemod.safely { Codemod.boot_meta }
|
|
427
|
-
applied_by_file[target_file] << candidate
|
|
428
|
-
else
|
|
429
|
-
live_meta[target_file] = original_text
|
|
430
|
-
File.write(target_file, original_text)
|
|
431
|
-
Codemod.safely { Codemod.boot_meta } # restore cache/memoized state to the reverted text
|
|
432
|
-
reason = error ? "reboot raised (#{error})" : "IR changed"
|
|
433
|
-
results[:skipped] << { file: "meta-domain", reason: reason, candidates: [@label.call(candidate)] }
|
|
434
|
-
end
|
|
435
|
-
end
|
|
436
|
-
|
|
437
|
-
applied_by_file.each do |file, candidates|
|
|
438
|
-
results[:applied] << { file: file, candidates: candidates.map(&@label) }
|
|
439
|
-
end
|
|
440
|
-
end
|
|
441
|
-
end
|
|
442
|
-
end
|
|
443
|
-
end
|