hecks 2.6.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.
Files changed (216) hide show
  1. checksums.yaml +4 -4
  2. data/exe/hecks +13 -0
  3. data/lib/hecks/adapters/driven/google_authentication.rb +1 -1
  4. data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
  5. data/lib/hecks/adapters/driven/memory.rb +1 -1
  6. data/lib/hecks/adapters/driven/postgres.rb +1 -1
  7. data/lib/hecks/adapters/driven/prism.rb +2 -2
  8. data/lib/hecks/adapters/driven/sqlite/projection.rb +1 -1
  9. data/lib/hecks/adapters/driven/tenant_provisioner.rb +31 -10
  10. data/lib/hecks/behaviors/expectations.rb +2 -2
  11. data/lib/hecks/bluebook/assembly/contract.rb +1 -1
  12. data/lib/hecks/bluebook/assembly/contracts.rb +5 -5
  13. data/lib/hecks/bluebook/assembly/specializer.rb +2 -2
  14. data/lib/hecks/bluebook/assembly.rb +1 -1
  15. data/lib/hecks/bluebook/behaviour/chapter.rb +5 -2
  16. data/lib/hecks/bluebook/behaviour/read_model.rb +18 -0
  17. data/lib/hecks/bluebook/capabilities.rb +2 -2
  18. data/lib/hecks/bluebook/chapter.rb +1 -1
  19. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +2 -2
  20. data/lib/hecks/bluebook/dsl/binding_proxy.rb +6 -1
  21. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +2 -2
  22. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -0
  23. data/lib/hecks/bluebook/dsl/command_builder.rb +3 -3
  24. data/lib/hecks/bluebook/dsl/entity_builder.rb +1 -1
  25. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +1 -1
  26. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +14 -9
  27. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +1 -1
  28. data/lib/hecks/bluebook/dsl/rule_reference.rb +3 -3
  29. data/lib/hecks/bluebook/dsl/translation_builder.rb +1 -1
  30. data/lib/hecks/bluebook/dsl/world_builder.rb +47 -3
  31. data/lib/hecks/bluebook/hexagon.rb +15 -7
  32. data/lib/hecks/bluebook/meta_validator/judge.rb +3 -3
  33. data/lib/hecks/bluebook/meta_validator/plan.rb +4 -4
  34. data/lib/hecks/bluebook/meta_validator/readings.rb +1 -1
  35. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +2 -2
  36. data/lib/hecks/bluebook/meta_validator/shapes.rb +1 -1
  37. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +11 -10
  38. data/lib/hecks/bluebook/meta_validator/world_judge.rb +3 -1
  39. data/lib/hecks/bluebook/meta_validator.rb +1 -1
  40. data/lib/hecks/bluebook/model_check/client_profile.rb +4 -70
  41. data/lib/hecks/bluebook/model_check.rb +13 -14
  42. data/lib/hecks/cache_dir.rb +103 -0
  43. data/lib/hecks/cli/document.rb +104 -0
  44. data/lib/hecks/cli/ir.rb +47 -0
  45. data/lib/hecks/cli/mcp.rb +32 -0
  46. data/lib/hecks/cli/mcp_door.rb +461 -0
  47. data/lib/hecks/cli/model_check.rb +270 -0
  48. data/lib/hecks/cli/project_cli.rb +123 -0
  49. data/lib/hecks/cli/project_diagrams.rb +43 -0
  50. data/lib/hecks/cli/run.rb +219 -0
  51. data/lib/hecks/cli/smoke_test.rb +59 -0
  52. data/lib/hecks/cli/stores.rb +54 -0
  53. data/lib/hecks/cli.rb +166 -0
  54. data/lib/hecks/embryonaut_bluebook/lock.rb +72 -0
  55. data/lib/hecks/embryonaut_bluebook/shape.rb +47 -0
  56. data/lib/hecks/embryonaut_bluebook/vendor.rb +194 -0
  57. data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +88 -0
  58. data/lib/hecks/embryonaut_bluebook.rb +29 -3
  59. data/lib/hecks/facade/handle.rb +2 -2
  60. data/lib/hecks/facade/surface/aggregate_door.rb +1 -1
  61. data/lib/hecks/forms/value_object_shape.rb +1 -1
  62. data/lib/hecks/framework/bluebook/console_settings.bluebook +3 -3
  63. data/lib/hecks/language/bluebook/vocabulary.bluebook +16 -0
  64. data/lib/hecks/language/world/world.bluebook +32 -2
  65. data/lib/hecks/mcp_door_scope.rb +176 -0
  66. data/lib/hecks/ports/key_vault.rb +1 -1
  67. data/lib/hecks/ports/persistence/append_only.rb +1 -1
  68. data/lib/hecks/ports/persistence/binding_policy.rb +16 -5
  69. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -2
  70. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +2 -2
  71. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +2 -2
  72. data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +132 -0
  73. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +1 -1
  74. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -2
  75. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +1 -1
  76. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +1 -1
  77. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +13 -7
  78. data/lib/hecks/ports/persistence/plugins/era.rb +7 -4
  79. data/lib/hecks/ports/persistence/postgres_dump/connection.rb +73 -0
  80. data/lib/hecks/ports/persistence/postgres_dump.rb +125 -0
  81. data/lib/hecks/ports/persistence/repository_factory.rb +2 -2
  82. data/lib/hecks/ports/persistence/state_codec.rb +1 -1
  83. data/lib/hecks/ports/persistence.rb +4 -0
  84. data/lib/hecks/projections/deploy/fargate/assembly.rb +193 -0
  85. data/lib/hecks/projections/deploy/fargate/cdn.rb +318 -0
  86. data/lib/hecks/projections/deploy/fargate/cdn_default.rb +87 -0
  87. data/lib/hecks/projections/deploy/fargate/check.rb +172 -0
  88. data/lib/hecks/projections/deploy/fargate/containers.rb +412 -0
  89. data/lib/hecks/projections/deploy/fargate/extras.rb +268 -0
  90. data/lib/hecks/projections/deploy/fargate/monitoring.rb +448 -0
  91. data/lib/hecks/projections/deploy/fargate/settings.rb +235 -0
  92. data/lib/hecks/projections/deploy/fargate/yaml.rb +116 -0
  93. data/lib/hecks/projections/deploy/fargate.rb +188 -139
  94. data/lib/hecks/projections/deploy/lambda.rb +8 -8
  95. data/lib/hecks/projections/deploy/preview/access_yaml.rb +145 -0
  96. data/lib/hecks/projections/deploy/preview/containers.rb +218 -0
  97. data/lib/hecks/projections/deploy/preview/preview.sh.erb +446 -0
  98. data/lib/hecks/projections/deploy/preview/routing_yaml.rb +216 -0
  99. data/lib/hecks/projections/deploy/preview/script.rb +40 -0
  100. data/lib/hecks/projections/deploy/preview/settings.rb +140 -0
  101. data/lib/hecks/projections/deploy/preview/task_yaml.rb +215 -0
  102. data/lib/hecks/projections/deploy/preview/template.rb +247 -0
  103. data/lib/hecks/projections/deploy/preview/yaml_text.rb +48 -0
  104. data/lib/hecks/projections/deploy/preview.rb +103 -0
  105. data/lib/hecks/projections/deploy/scripts/deploy-service.sh.tmpl +182 -0
  106. data/lib/hecks/projections/deploy/scripts/expected-era.header +17 -0
  107. data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +54 -0
  108. data/lib/hecks/projections/deploy/scripts/settings.rb +114 -0
  109. data/lib/hecks/projections/deploy/scripts/smoke-after-deploy.sh.tmpl +232 -0
  110. data/lib/hecks/projections/deploy/scripts.rb +171 -0
  111. data/lib/hecks/projections/deploy/smoke/harness.js +366 -0
  112. data/lib/hecks/projections/deploy/smoke/workflow.yml.tmpl +56 -0
  113. data/lib/hecks/projections/deploy/smoke.rb +159 -0
  114. data/lib/hecks/projections/deploy/template_diff/comparer.rb +231 -0
  115. data/lib/hecks/projections/deploy/template_diff/loader.rb +105 -0
  116. data/lib/hecks/projections/deploy/template_diff.rb +173 -0
  117. data/lib/hecks/projections/glossary/sensitivity.rb +1 -1
  118. data/lib/hecks/projections/reference.rb +7 -1
  119. data/lib/hecks/projections.rb +2 -0
  120. data/lib/hecks/projector/exporter.rb +4 -4
  121. data/lib/hecks/projector/target.rb +1 -1
  122. data/lib/hecks/release/gem_pin/published_versions.rb +58 -0
  123. data/lib/hecks/release/gem_pin.rb +112 -0
  124. data/lib/hecks/release/runner/ci_publisher.rb +96 -0
  125. data/lib/hecks/release/runner/commands.rb +68 -0
  126. data/lib/hecks/release/runner/console.rb +52 -0
  127. data/lib/hecks/release/runner/gem_publisher.rb +44 -0
  128. data/lib/hecks/release/runner/git.rb +48 -0
  129. data/lib/hecks/release/runner/npm_publisher.rb +89 -0
  130. data/lib/hecks/release/runner/preflight.rb +119 -0
  131. data/lib/hecks/release/runner/published.rb +55 -0
  132. data/lib/hecks/release/runner/tagger.rb +87 -0
  133. data/lib/hecks/release/runner.rb +244 -0
  134. data/lib/hecks/router/namespace_installer.rb +1 -1
  135. data/lib/hecks/runtime/command_interpreter.rb +3 -3
  136. data/lib/hecks/runtime/command_rules/admissibility.rb +2 -2
  137. data/lib/hecks/runtime/command_rules/arithmetic.rb +2 -2
  138. data/lib/hecks/runtime/entity_element.rb +1 -1
  139. data/lib/hecks/runtime/entity_interpreter.rb +1 -1
  140. data/lib/hecks/runtime/invocation.rb +1 -1
  141. data/lib/hecks/runtime/loader.rb +27 -3
  142. data/lib/hecks/runtime/outbox.rb +1 -1
  143. data/lib/hecks/runtime/reaction_invocation.rb +3 -3
  144. data/lib/hecks/runtime/read_model_interpreter.rb +37 -15
  145. data/lib/hecks/runtime/reference_hop.rb +4 -4
  146. data/lib/hecks/runtime/registry/saga_persistence.rb +8 -2
  147. data/lib/hecks/runtime/registry/verification.rb +15 -13
  148. data/lib/hecks/runtime/registry/world_defaults.rb +106 -0
  149. data/lib/hecks/runtime/registry.rb +22 -13
  150. data/lib/hecks/runtime/saga_interpreter.rb +1 -1
  151. data/lib/hecks/runtime/tenant_check.rb +1 -1
  152. data/lib/hecks/runtime/value/coercion.rb +3 -3
  153. data/lib/hecks/runtime/value/entity_list_coercion.rb +1 -1
  154. data/lib/hecks/storehouse.rb +18 -12
  155. data/lib/hecks/vendoring/git_environment.rb +35 -0
  156. data/lib/hecks/vendoring/git_source.rb +102 -0
  157. data/lib/hecks/vendoring.rb +102 -0
  158. data/lib/hecks/version.rb +1 -1
  159. data/lib/hecks/vocabulary.rb +5 -0
  160. data/lib/hecks.rb +8 -2
  161. metadata +72 -58
  162. data/lib/hecks/bench/cli.rb +0 -74
  163. data/lib/hecks/bench/environment.rb +0 -100
  164. data/lib/hecks/bench/postgres_probe.rb +0 -41
  165. data/lib/hecks/bench/report.rb +0 -116
  166. data/lib/hecks/bench/ruby_runner.rb +0 -151
  167. data/lib/hecks/bench/run.rb +0 -55
  168. data/lib/hecks/bench/rust_runner.rb +0 -165
  169. data/lib/hecks/bench/stats.rb +0 -75
  170. data/lib/hecks/bench/suite.rb +0 -172
  171. data/lib/hecks/bench/workload.rb +0 -137
  172. data/lib/hecks/bench.rb +0 -28
  173. data/lib/hecks/codemod.rb +0 -443
  174. data/lib/hecks/corpus.rb +0 -479
  175. data/lib/hecks/doc/reference.rb +0 -585
  176. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -684
  177. data/lib/hecks/fuzzing/combination_miner.rb +0 -178
  178. data/lib/hecks/fuzzing/concurrent_dispatch.rb +0 -469
  179. data/lib/hecks/fuzzing/coverage_campaign.rb +0 -161
  180. data/lib/hecks/fuzzing/differential.rb +0 -192
  181. data/lib/hecks/fuzzing/domain_generator.rb +0 -871
  182. data/lib/hecks/fuzzing/era_boundary.rb +0 -154
  183. data/lib/hecks/fuzzing/form_census.rb +0 -285
  184. data/lib/hecks/fuzzing/generated_domain_check.rb +0 -171
  185. data/lib/hecks/fuzzing/invalid_value_generator.rb +0 -125
  186. data/lib/hecks/fuzzing/isolated_boot.rb +0 -604
  187. data/lib/hecks/fuzzing/nondeterministic.rb +0 -79
  188. data/lib/hecks/fuzzing/persistence_parity.rb +0 -253
  189. data/lib/hecks/fuzzing/properties/corrections.rb +0 -125
  190. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +0 -688
  191. data/lib/hecks/fuzzing/properties/guards.rb +0 -398
  192. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +0 -399
  193. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +0 -137
  194. data/lib/hecks/fuzzing/properties/outbox.rb +0 -180
  195. data/lib/hecks/fuzzing/properties/querying.rb +0 -260
  196. data/lib/hecks/fuzzing/properties.rb +0 -267
  197. data/lib/hecks/fuzzing/qa_settings.rb +0 -164
  198. data/lib/hecks/fuzzing/replay.rb +0 -930
  199. data/lib/hecks/fuzzing/rotation_priority.rb +0 -114
  200. data/lib/hecks/fuzzing/rust_gap_manifest.rb +0 -139
  201. data/lib/hecks/fuzzing/self_consistency.rb +0 -825
  202. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +0 -532
  203. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +0 -171
  204. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +0 -127
  205. data/lib/hecks/fuzzing/sequence_generator/picker.rb +0 -116
  206. data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +0 -138
  207. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +0 -344
  208. data/lib/hecks/fuzzing/sequence_generator.rb +0 -311
  209. data/lib/hecks/fuzzing/shrinker.rb +0 -309
  210. data/lib/hecks/fuzzing/structural_skips.rb +0 -53
  211. data/lib/hecks/fuzzing/sweep_depth.rb +0 -61
  212. data/lib/hecks/fuzzing/target_capabilities.rb +0 -255
  213. data/lib/hecks/fuzzing/value_generator.rb +0 -340
  214. data/lib/hecks/fuzzing.rb +0 -23
  215. data/lib/hecks/grammar/evolve.rb +0 -524
  216. 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