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
@@ -97,9 +97,10 @@ module Hecks
97
97
  include Hecks::IR
98
98
  include Behaviour::World
99
99
 
100
- emits_ir(domain: :domain, realm: :realm, latest: :latest, settings: :settings)
100
+ emits_ir(domain: :domain, realm: :realm, latest: :latest, settings: :settings,
101
+ default_database: :default_database, default_adapter: :default_adapter)
101
102
 
102
- attr_reader :domain, :realm, :latest, :settings
103
+ attr_reader :domain, :realm, :latest, :settings, :default_database, :default_adapter
103
104
 
104
105
  # @param domain [String, Symbol] the domain this world configures
105
106
  # @param realm [String, Symbol, nil] the declared realm/version marker, or `nil`
@@ -108,11 +109,18 @@ module Hecks
108
109
  # if none is declared
109
110
  # @param settings [Hash] the declared adapter bind settings, keyed by verb and,
110
111
  # for a qualified entry, `"verb:adapter"`
111
- def initialize(domain:, realm: nil, latest: nil, settings: {})
112
- @domain = domain.to_s
113
- @realm = realm&.to_s
114
- @latest = latest&.to_s
115
- @settings = settings
112
+ # @param default_database [String, nil] the connection every chapter's
113
+ # database-taking persistence adapter uses unless its own settings name one,
114
+ # or `nil` if none is declared
115
+ # @param default_adapter [String, nil] the persistence adapter every aggregate
116
+ # binds to unless its chapter's hecksagon binds it, or `nil` if none is declared
117
+ def initialize(domain:, realm: nil, latest: nil, settings: {}, default_database: nil, default_adapter: nil)
118
+ @domain = domain.to_s
119
+ @realm = realm&.to_s
120
+ @latest = latest&.to_s
121
+ @settings = settings
122
+ @default_database = default_database&.to_s
123
+ @default_adapter = default_adapter&.to_s
116
124
  end
117
125
  end
118
126
  end
@@ -302,7 +302,7 @@ module Hecks
302
302
  end
303
303
 
304
304
  # Entity-owned categories have no top-level aggregate for the runtime
305
- # to route a bare verb into any more — `Member`'s own "Pair" reaches
305
+ # to route a bare verb into — `Member`'s own "Pair" reaches
306
306
  # the runtime as `ValueObject.Member.Pair`, and a nested one
307
307
  # (`Dispatch`, inside `Handler`) reaches it as `ProcessManager.
308
308
  # Handler.Dispatch.Bind` — the dotted shape `EntityInterpreter#call`
@@ -567,8 +567,8 @@ module Hecks
567
567
  # Reference its type is Reference<X>, another aggregate's head
568
568
  # Holds its type names an entity this aggregate declares
569
569
  #
570
- # Nothing is skipped any more. Reference and Holds did not exist, so the
571
- # walk dropped both kinds and the meta-domain silently did not contain
570
+ # Nothing is skipped. Without Reference and Holds the walk would drop
571
+ # both kinds and the meta-domain would silently not contain
572
572
  # Account#customer_id or Account#ledger.
573
573
  # Each alternate carries its own map, read from the language. Borrowing the
574
574
  # primary's map dispatched `type:` where Reference declares `points_at:`,
@@ -108,7 +108,7 @@ module Hecks
108
108
  # S17, ADR 0026 — Member/Handler/Dispatch are entities now
109
109
  # (`entity "Member" do ... end`, nested under `ValueObject`/
110
110
  # `ProcessManager`/`Handler`), not their own top-level
111
- # aggregates — so `meta.aggregates` alone no longer finds
111
+ # aggregates — so `meta.aggregates` alone does not find
112
112
  # them the way it always found a standalone `aggregate
113
113
  # "Member"`. Each still needs its own named category here:
114
114
  # `Assembly::Contracts` keeps a bespoke entry for each
@@ -125,8 +125,8 @@ module Hecks
125
125
  # itself ever declares are exactly the three this ADR
126
126
  # names. `entity_owned: true` records why this is a
127
127
  # category at all — the real runtime has no top-level
128
- # aggregate named "Member" to dispatch a bare verb into
129
- # any more, so the judge has to build a dotted one instead
128
+ # aggregate named "Member" to dispatch a bare verb into,
129
+ # so the judge has to build a dotted one instead
130
130
  # (see `Judge#verb_for`).
131
131
  #
132
132
  # Recurses — `Dispatch` nests inside `Handler`, which nests
@@ -158,7 +158,7 @@ module Hecks
158
158
  # Every verb the language declares, spelled as the judge would
159
159
  # dispatch it. S17, ADR 0026 — an entity-owned category (its
160
160
  # own `entity_owned` flag) has no real top-level aggregate the
161
- # runtime can route a bare verb into any more, so it is
161
+ # runtime can route a bare verb into, so it is
162
162
  # spelled dotted here too — `Bluebook::ValueObject.Member.
163
163
  # Declare`, `Bluebook::ProcessManager.Handler.Dispatch.Bind` —
164
164
  # matching `Judge#verb_for`/`#dotted_prefix`'s own build
@@ -364,7 +364,7 @@ module Hecks
364
364
  # same way Shapes#provenance reads it back.
365
365
  return encode_literal(node.provenance) if field == :provenance
366
366
 
367
- # `identified_by` is no longer a field of any declaration — it is a list,
367
+ # `identified_by` is not a field of any declaration — it is a list,
368
368
  # filled by Identify one part at a time, so it is read through `identity_rows`
369
369
  # like every other list rather than special-cased here. What this branch
370
370
  # existed to protect is now structural : a path cannot come back as its head,
@@ -228,7 +228,7 @@ module Hecks
228
228
  # by its own dotted `ValueObject.Member.Pair`. Its data therefore
229
229
  # lives inline on the value object's own dispatched state — same as
230
230
  # any other entity list — not behind a separate `DeclaredIn` query:
231
- # there is no such query any more, because there is no top-level
231
+ # there is no such query, because there is no top-level
232
232
  # "Member" aggregate left to hold one. Pairs are still an open map,
233
233
  # which no value object can hold, so they still come back one pair
234
234
  # at a time.
@@ -330,7 +330,7 @@ module Hecks
330
330
  # Handler.Dispatch`/`...Dispatch.Bind`. Its data therefore lives
331
331
  # inline on the process manager's own dispatched state — same as
332
332
  # any other entity list — not behind a separate `DeclaredIn`
333
- # query : there is no such query any more, the same fix
333
+ # query : there is no such query, the same fix
334
334
  # `Reconstruction#members_of` already made for Member.
335
335
  def process_manager(row)
336
336
  declaration("ProcessManager", row,
@@ -32,7 +32,7 @@ module Hecks
32
32
  default: decode_literal(text(field[:default])),
33
33
  # Read back the same way `list` is — both are booleans about the
34
34
  # attribute, held as text, and dropping either would rebuild a
35
- # bluebook that no longer says what it said.
35
+ # bluebook that says something other than what was written.
36
36
  optional: text(field[:optional]).to_s == "true",
37
37
  pattern: presence(text(field[:pattern])),
38
38
  # The round trip is the only way in. The grammar registry keeps the
@@ -1,5 +1,6 @@
1
1
  require "digest"
2
2
  require "fileutils"
3
+ require_relative "../../cache_dir"
3
4
 
4
5
  module Hecks
5
6
  module Bluebook
@@ -145,13 +146,10 @@ module Hecks
145
146
  #
146
147
  # Fails toward a real boot, never toward a wrong table — same
147
148
  # loud-not-silent discipline this codebase already holds CI to
148
- # (`postgres_io_relevant_changed`'s own header). A missing file, a
149
- # corrupt Marshal blob, a permission error, an unwritable `tmp/` —
150
- # every one of these degrades to "no disk cache today", never to a
151
- # crash or a served-but-wrong table. `tmp/` is already gitignored
152
- # for exactly this kind of local, disposable-but-useful-while-it-
153
- # lasts artifact (`Storehouse::LOG_ROOT`'s own header names the
154
- # same convention).
149
+ # (`postgres_io_relevant_changed`'s own header). A missing file, a corrupt
150
+ # Marshal blob, a permission error or an unwritable cache directory degrades
151
+ # to "no disk cache today", never to a crash or a served-but-wrong table.
152
+ # The cache lives under `Hecks::CacheDir`, never the gem's read-only directory.
155
153
  #
156
154
  # **Atomic write, not a lock** — `qa_sweep --all` spawns up to 4
157
155
  # children at once, any of which could reach a cold cache
@@ -166,7 +164,10 @@ module Hecks
166
164
  # change, the same shape `Storehouse::BOOT_ROOT`'s own
167
165
  # `HECKS_STOREHOUSE_ROOT` override uses, for the day this needs to
168
166
  # be ruled out while debugging something else entirely.
169
- CACHE_DIR = File.expand_path("../../../../tmp/hecks_syntax_boot_cache", __dir__).freeze
167
+ #
168
+ # @return [String] the directory cache entries are written to:
169
+ # `hecks_syntax_boot_cache` under `Hecks::CacheDir`
170
+ def cache_dir = Hecks::CacheDir.path("hecks_syntax_boot_cache")
170
171
 
171
172
  # Reports whether the cross-process disk cache is turned on.
172
173
  #
@@ -203,7 +204,7 @@ module Hecks
203
204
  return unless disk_cache_enabled?
204
205
 
205
206
  path = disk_cache_path(chapters)
206
- FileUtils.mkdir_p(CACHE_DIR)
207
+ FileUtils.mkdir_p(cache_dir)
207
208
  tmp_path = "#{path}.#{Process.pid}.tmp"
208
209
  File.binwrite(tmp_path, Marshal.dump(result))
209
210
  File.rename(tmp_path, path)
@@ -217,7 +218,7 @@ module Hecks
217
218
  # chapter]` pairs to key the cache entry on
218
219
  # @return [String] the disk cache file path for `chapters`
219
220
  def disk_cache_path(chapters)
220
- File.join(CACHE_DIR, "#{disk_cache_key(chapters)}.marshal")
221
+ File.join(cache_dir, "#{disk_cache_key(chapters)}.marshal")
221
222
  end
222
223
 
223
224
  # Builds the cache key for `chapters`.
@@ -40,7 +40,9 @@ module Hecks
40
40
  def judge!
41
41
  domain = @world.domain
42
42
  send_to("World::World.Declare", domain, domain: v(domain),
43
- realm: v(@world.realm), latest: v(@world.latest))
43
+ realm: v(@world.realm), latest: v(@world.latest),
44
+ default_database: v(@world.default_database),
45
+ default_adapter: v(@world.default_adapter))
44
46
 
45
47
  Hash(@world.settings).each do |verb, values|
46
48
  # the DSL records each binding twice — once by verb, once by
@@ -541,7 +541,7 @@ module Hecks
541
541
  # every Paging-attached word (limit/offset/cursor/nulls) because
542
542
  # something called it inside this exact window.
543
543
  #
544
- # SyntaxBoot.call no longer uses this. Gating its cache on "the
544
+ # SyntaxBoot.call does not use this. Gating its cache on "the
545
545
  # whole registry is finished" meant nothing was cached for the
546
546
  # entire window, and the window is not narrow — every word routed
547
547
  # through `word_gate_dispatch` while the language judged itself
@@ -15,60 +15,29 @@ module Hecks
15
15
  #
16
16
  # ## The rules
17
17
  #
18
- # - `:client_group_by_row_drop` — a `group_by` whose fields do not include
19
- # the grouped aggregate's whole identity. Rows sharing a key path are
20
- # collapsed to the first, on every adapter (docs/decisions/0061).
21
18
  # - `:client_native_read_model` — a read model the SQLite projection
22
19
  # answers natively, where every other path answers it in process
23
20
  # (docs/1.0-readiness.md, "Known gaps at 1.0", item 2).
24
- # - `:client_dotted_compute_source` — an era translation `compute` whose
25
- # source is a dotted path, which never fires
26
- # (`Translation::RuleCompiler.compile_compute`).
27
21
  module ClientProfile
28
22
  # Adapters whose Ruby class implements `query_read_model`, which is what
29
23
  # makes `ReadModelInterpreter#project` take the native path. The spec
30
24
  # checks this list against the adapter classes themselves.
31
25
  NATIVE_READ_MODEL_ADAPTERS = %w[SqliteProjection].freeze
32
26
 
33
- GROUP_BY_TRACKER = "docs/decisions/0061-query-dsl-aggregation-count-sum-group-by.md".freeze
34
27
  NATIVE_READ_MODEL_TRACKER = "docs/1.0-readiness.md (Known gaps at 1.0, item 2)".freeze
35
- DOTTED_COMPUTE_TRACKER = "Translation::RuleCompiler.compile_compute, and the pending example " \
36
- "\"applies the SQL of a compute whose source is a dotted member\" in " \
37
- "spec/adapters/driven/postgres_era/migration_data_safety_spec.rb".freeze
38
28
 
39
29
  module_function
40
30
 
41
- # Runs every client-profile rule over one chapter and its translations.
31
+ # Runs every client-profile rule over one chapter.
42
32
  #
43
33
  # @param bluebook [Bluebook::Chapter] the assembled chapter to check
44
34
  # @param hecksagon [Bluebook::Hecksagon, nil] the chapter's sibling wiring file; without
45
35
  # one no aggregate is projected, so the native-read-model rule has nothing to find
46
- # @param translations [Array<Bluebook::Translation>] the data translations declared for
47
- # this chapter's domain
48
36
  # @return [Array<ModelCheck::Finding>] one error-severity finding per construct refused
49
- def call(bluebook, hecksagon: nil, translations: [])
37
+ def call(bluebook, hecksagon: nil)
50
38
  bluebook.read_models.flat_map do |model|
51
- group_by_findings(bluebook, model) + native_read_model_findings(model, hecksagon)
52
- end + dotted_compute_findings(translations)
53
- end
54
-
55
- # Refuses a `group_by` that cannot prove each of its key paths names one row.
56
- #
57
- # The interpreter's `nest` keeps `stripped.first` per key path, whichever adapter
58
- # supplied the rows, because a `group_by` model never reaches the native path. Grouping
59
- # by the aggregate's whole identity is the one shape that cannot collide.
60
- #
61
- # @param bluebook [Bluebook::Chapter] the chapter the read model is declared in
62
- # @param model [Bluebook::ReadModel] the read model to inspect
63
- # @return [Array<ModelCheck::Finding>] one finding, or `[]` when `model` declares no
64
- # `group_by` or groups by its aggregate's whole identity
65
- def group_by_findings(bluebook, model)
66
- return [] if model.group_by.empty? || groups_by_identity?(bluebook, model)
67
-
68
- [finding(:client_group_by_row_drop, model.name,
69
- "group_by #{model.group_by_fields.map(&:inspect).join(', ')} does not cover the grouped " \
70
- "aggregate's whole identity, so rows sharing a key path are silently reduced to the " \
71
- "first. Tracked in #{GROUP_BY_TRACKER}.")]
39
+ native_read_model_findings(model, hecksagon)
40
+ end
72
41
  end
73
42
 
74
43
  # Refuses a read model that the SQLite projection would answer natively.
@@ -95,26 +64,6 @@ module Hecks
95
64
  "is not, and nothing checks the two agree. Tracked in #{NATIVE_READ_MODEL_TRACKER}.")]
96
65
  end
97
66
 
98
- # Refuses a translation `compute` whose source names a member of a value object.
99
- #
100
- # `compile_compute` guards the compute with `__s ? '<from>'`, a top-level key test, so a
101
- # dotted source never matches, the mint succeeds and the record keeps its old value.
102
- #
103
- # @param translations [Array<Bluebook::Translation>] the translations to inspect
104
- # @return [Array<ModelCheck::Finding>] one finding per dotted-source compute
105
- def dotted_compute_findings(translations)
106
- translations.flat_map do |translation|
107
- translation.aggregates.flat_map do |aggregate|
108
- aggregate.computes.select { |compute| compute.from.to_s.include?(".") }.map do |compute|
109
- finding(:client_dotted_compute_source, "#{translation.domain}::#{aggregate.name}",
110
- "compute #{compute.from.to_s.inspect} has a dotted source, which the compiled SQL " \
111
- "never matches, so the mint succeeds and leaves the old value in place. " \
112
- "Tracked in #{DOTTED_COMPUTE_TRACKER}.")
113
- end
114
- end
115
- end
116
- end
117
-
118
67
  # Reports whether a model's shape sends it down the native path in `#project`.
119
68
  #
120
69
  # @param model [Bluebook::ReadModel] the read model to inspect
@@ -123,21 +72,6 @@ module Hecks
123
72
  !model.reference_target.nil? && model.group_by.empty? && !model.count? && !model.median_field
124
73
  end
125
74
 
126
- # Reports whether a `group_by` names every identity path of the aggregate it groups.
127
- #
128
- # @param bluebook [Bluebook::Chapter] the chapter the read model is declared in
129
- # @param model [Bluebook::ReadModel] a read model that declares a `group_by`
130
- # @return [Boolean] false when the many-side aggregate is not a top-level aggregate or
131
- # declares no identity, since uniqueness cannot then be shown
132
- def groups_by_identity?(bluebook, model)
133
- target = model.aggregate_heads.find { |head| head[:many] }
134
- aggregate = target && bluebook.aggregate(target[:aggregate])
135
- return false unless aggregate
136
-
137
- identity = Array(aggregate.identity_heads).map(&:to_sym)
138
- !identity.empty? && (identity - model.group_by_fields.map(&:to_sym)).empty?
139
- end
140
-
141
75
  # Builds one error-severity finding of this profile.
142
76
  #
143
77
  # @param kind [Symbol] the finding's kind, one of the `:client_*` rules above
@@ -25,7 +25,7 @@ module Hecks
25
25
  # **A finding shipped, not silenced** — the coverage-gate idiom, empty
26
26
  # allowlists enforced both directions (spec/model_check_spec.rb holds
27
27
  # this exact table: an error the checker reports and this does not
28
- # name is a regression, an entry the checker no longer reports is
28
+ # name is a regression, an entry the checker does not report is
29
29
  # stale and must be deleted). bin/model_check reads this same
30
30
  # constant, so the tool and the spec can never drift apart.
31
31
  #
@@ -44,9 +44,9 @@ module Hecks
44
44
  # was a `state "x"` line never named by any handler's own from:/to:, a
45
45
  # pure declaration-drift artifact. States are derived from the
46
46
  # transitions that name them now (ProcessManagerBuilder#derived_
47
- # states), so a state nothing ever transitions into or out of no
48
- # longer exists to be unreachable — the finding that entry allowlisted
49
- # cannot occur any more, by construction.
47
+ # states), so a state nothing ever transitions into or out of does
48
+ # not exist to be unreachable — the finding that entry allowlisted
49
+ # cannot occur, by construction.
50
50
  #
51
51
  # "banking"/NotifyOnClosure, FlagKeyReturn — gone from here, moved
52
52
  # to banking. `across "Notifications"` names a domain that does not
@@ -145,15 +145,11 @@ module Hecks
145
145
  # findings for constructs known to answer wrongly without refusing;
146
146
  # `nil` (the default) adds nothing and leaves every other finding as
147
147
  # it was
148
- # @param translations [Array<Bluebook::Translation>] the data
149
- # translations declared for this chapter's domain, read only by the
150
- # `:client` profile — they live on the registry, not the chapter, so
151
- # the caller supplies them the way it supplies `hecksagon`
152
148
  # @return [Array<Finding>] every finding this bluebook triggers,
153
149
  # across its lifecycles, sagas, policies, and Rust-reserved names
154
150
  # @raise [ArgumentError] if `profile` is neither `nil` nor `:client`
155
151
  def call(bluebook, hecksagon: nil, known_domains: nil, global_emitted_events: nil, rust_target: false, strict: false,
156
- profile: nil, translations: [])
152
+ profile: nil)
157
153
  unless profile.nil? || PROFILES.include?(profile)
158
154
  raise ArgumentError, "unknown profile #{profile.inspect} (known: #{PROFILES.inspect})"
159
155
  end
@@ -170,7 +166,7 @@ module Hecks
170
166
  findings.concat(rust_reserved_name_findings(domain_name: bluebook.name,
171
167
  aggregate_names: bluebook.aggregates.map(&:hecks_name),
172
168
  rust_target: rust_target, strict: strict))
173
- findings.concat(ClientProfile.call(bluebook, hecksagon: hecksagon, translations: translations)) if profile == :client
169
+ findings.concat(ClientProfile.call(bluebook, hecksagon: hecksagon)) if profile == :client
174
170
  findings
175
171
  end
176
172
 
@@ -659,6 +655,9 @@ module Hecks
659
655
  # sibling wiring file, if the caller loaded one
660
656
  # @param known_domains [Set<String>, nil] every bluebook/hecksagon
661
657
  # name the caller has booted anywhere in this corpus scan
658
+ # @param global_emitted_events [Set<String>, nil] every event name emitted anywhere in the
659
+ # corpus, consulted only as a fallback for a `translates` reaction; nil when the caller
660
+ # has no cross-domain set to offer
662
661
  # @return [Array<Finding>] every `deaf_policy`/`unknown_trigger`
663
662
  # finding this policy triggers, or `cross_domain_policy_findings`'s
664
663
  # own return for a cross-domain policy
@@ -680,12 +679,12 @@ module Hecks
680
679
  # (`across "Compliance"`), which exits this method one line above
681
680
  # before the mismatch is ever reached.
682
681
  # `global_emitted_events` — a `translates` (hecksagon-level)
683
- # reaction is BUILT as the exact same same-domain-target `Policy`
682
+ # reaction is built as the exact same same-domain-target `Policy`
684
683
  # an ordinary `policy` block is (no `target_domain`, so it never
685
684
  # reaches `cross_domain_policy_findings` above), but its whole
686
- # point is to react to a FOREIGN domain's own event — this
685
+ # point is to react to a foreign domain's own event — this
687
686
  # domain's own `emitted_events(bluebook)` was never going to
688
- # contain it. Checked only as a FALLBACK, after the local check
687
+ # contain it. Checked only as a fallback, after the local check
689
688
  # already failed, so a real same-domain typo still gets flagged
690
689
  # exactly as before whenever the caller has no cross-domain set
691
690
  # to offer (nil — every existing call site, unchanged) or the
@@ -793,7 +792,7 @@ module Hecks
793
792
 
794
793
  # **Typo detection, deliberately weaker** — `known_domains` can only
795
794
  # ever be a monorepo-scoped heuristic: a real external hecks
796
- # consumer's own domain (this repo's own embryonaut/lifeadelics-
795
+ # consumer's own domain (this repo's own embryonaut client-
797
796
  # shaped case) lives in a genuinely separate repository this
798
797
  # corpus scan can never see, so a target this check cannot find
799
798
  # is "unknown to THIS corpus," never proof of a typo. A target
@@ -0,0 +1,103 @@
1
+ require "etc"
2
+ require "fileutils"
3
+ require "tmpdir"
4
+
5
+ module Hecks
6
+ # The one place the runtime's own scratch files live, outside the gem.
7
+ #
8
+ # The syntax-boot cache and the Storehouse audit log are written on a
9
+ # normal run, so they cannot sit under the gem's own directory: an
10
+ # installed gem is read-only, and a checkout's `tmp/` is not a place a
11
+ # library gets to assume. Both ask this module for a subdirectory instead.
12
+ #
13
+ # ## Where the root is
14
+ #
15
+ # The first of these that can be made a private directory wins:
16
+ #
17
+ # 1. `$XDG_CACHE_HOME/hecks`, when `XDG_CACHE_HOME` is set to an absolute path
18
+ # 2. `~/.cache/hecks`
19
+ # 3. `<system temp dir>/hecks-<uid>`
20
+ #
21
+ # ## Private, or not used
22
+ #
23
+ # The syntax-boot cache is read back with `Marshal.load`, so a directory
24
+ # another account can write to is a way to run code as this one. A
25
+ # candidate is accepted only if it is owned by the current user and
26
+ # writable by nobody else; the shared temp directory in particular is
27
+ # where a predictable name can be planted by someone else. When no candidate
28
+ # qualifies, a fresh per-process directory stands in: the cache still
29
+ # works within the process and is never shared with a stranger.
30
+ module CacheDir
31
+ module_function
32
+
33
+ # The directory a named piece of scratch state lives in.
34
+ #
35
+ # @param name [String] the subdirectory, such as `"storehouse"`
36
+ # @return [String] the absolute path of `name` under the cache root; the
37
+ # directory itself is created by whoever writes to it
38
+ def path(name)
39
+ File.join(root, name)
40
+ end
41
+
42
+ # The cache root, resolved once per process.
43
+ #
44
+ # @return [String] the absolute path of the directory every `path` lives under
45
+ def root
46
+ @root ||= resolve
47
+ end
48
+
49
+ # Forgets the resolved root so the next call resolves again.
50
+ #
51
+ # @return [void]
52
+ def reset!
53
+ @root = nil
54
+ end
55
+
56
+ # Picks the cache root from the environment.
57
+ #
58
+ # @param env [Hash{String => String}] the environment to read
59
+ # `XDG_CACHE_HOME` and `HOME` from
60
+ # @param tmpdir [String] the system temp directory
61
+ # @param uid [Integer] the current user id, used in the temp fallback's name
62
+ # @return [String] the absolute path of the first candidate that is a
63
+ # private directory owned by this user, else a per-process directory
64
+ def resolve(env: ENV, tmpdir: Dir.tmpdir, uid: Process.uid)
65
+ candidates(env, tmpdir, uid).each do |candidate|
66
+ return candidate if private_dir?(candidate)
67
+ end
68
+ Dir.mktmpdir("hecks-", tmpdir)
69
+ rescue StandardError
70
+ File.join(tmpdir, "hecks-#{uid}")
71
+ end
72
+
73
+ # The roots to try, most preferred first.
74
+ #
75
+ # @param env [Hash{String => String}] the environment
76
+ # @param tmpdir [String] the system temp directory
77
+ # @param uid [Integer] the current user id
78
+ # @return [Array<String>] absolute candidate paths
79
+ def candidates(env, tmpdir, uid)
80
+ xdg = env["XDG_CACHE_HOME"].to_s
81
+ home = env["HOME"].to_s
82
+ list = []
83
+ list << File.join(xdg, "hecks") if File.absolute_path?(xdg)
84
+ list << File.join(home, ".cache", "hecks") if File.absolute_path?(home)
85
+ list << File.join(tmpdir, "hecks-#{uid}")
86
+ end
87
+ private_class_method :candidates
88
+
89
+ # Creates `dir` if needed and says whether it is private to this user.
90
+ #
91
+ # @param dir [String] the candidate directory
92
+ # @return [Boolean] true when `dir` is a directory owned by this user that
93
+ # no other user can write to; false on any failure
94
+ def private_dir?(dir)
95
+ FileUtils.mkdir_p(dir, mode: 0o700)
96
+ stat = File.stat(dir)
97
+ stat.directory? && stat.owned? && stat.mode.nobits?(0o022) && File.writable?(dir)
98
+ rescue StandardError
99
+ false
100
+ end
101
+ private_class_method :private_dir?
102
+ end
103
+ end
@@ -0,0 +1,104 @@
1
+ require_relative "../../hecks"
2
+
3
+ module Hecks
4
+ module CLI
5
+ # The command behind `bin/docs`, `bin/narrate`, `hecks docs` and `hecks narrate`:
6
+ # one domain's document, projected from its own bluebook, to stdout.
7
+ #
8
+ # `:docs` answers the usage document a console's `<Domain>.docs` prints; `:narrate`
9
+ # answers the same chapter read back in English, for the subject-matter expert
10
+ # (`Projector::NarrateProjector`). Nothing is written for the caller: redirect it,
11
+ # so the caller decides where a generated file lands and git decides whether it
12
+ # drifted.
13
+ #
14
+ # ## Every failure is a sentence
15
+ #
16
+ # A misspelled aggregate refuses by name rather than printing nothing and exiting
17
+ # 0. A caller cannot tell an empty document from an empty domain, so this refuses
18
+ # instead and names what is actually there.
19
+ #
20
+ # ## Standing in a domain is enough
21
+ #
22
+ # `Adapters::Folder#domain_root` walks up for the nearest `.hecksagon`, which makes
23
+ # the first argument ambiguous. The rule: a directory is a domain, anything else
24
+ # is an aggregate name, and the two-argument form says which is which.
25
+ module Document
26
+ # Build output and other trees under a root that hold no domain a caller means.
27
+ # Matched against the path relative to the root, because a substring test
28
+ # against an absolute path tests wherever somebody happened to clone.
29
+ IGNORED = %r{\A(rust|deploy|tmp|coverage)/}
30
+
31
+ module_function
32
+
33
+ # Prints `projection` for the domain and optional aggregate `argv` names.
34
+ #
35
+ # @param argv [Array<String>] an optional domain directory, then an optional
36
+ # aggregate name
37
+ # @param projection [Symbol] `:docs` or `:narrate`
38
+ # @param program [String] the name the usage message calls this command by
39
+ # @param root [String] the directory whose domains are listed when no domain
40
+ # is found
41
+ # @return [void]
42
+ # @raise [SystemExit] when no domain is found, it cannot boot, it loads no
43
+ # bluebook, or the aggregate does not exist
44
+ def call(argv, projection:, program:, root:)
45
+ here = Adapters::Folder.new.domain_root
46
+ path, aggregate = target(argv, here)
47
+ refuse_without_domain(program, root) unless path
48
+
49
+ begin
50
+ runtime = Hecks.boot(path, install_facade: false)
51
+ rescue StandardError => e
52
+ abort "cannot read #{path}: #{e.message.lines.first.strip}"
53
+ end
54
+
55
+ # The domain's own chapter, not a framework member it attached: insertion
56
+ # order, the same distinction `Runtime::Loader.dispatcher_for` draws.
57
+ bluebook = runtime.registry.bluebooks.values.first
58
+ abort "#{path} loaded no bluebook" unless bluebook
59
+
60
+ begin
61
+ puts Projector.call(projection, bluebook: bluebook, options: aggregate ? { aggregate: aggregate } : {})
62
+ rescue Runtime::NotFound => e
63
+ abort e.message
64
+ end
65
+ end
66
+
67
+ # Lists every directory under `root` that `Hecks.boot` would accept.
68
+ #
69
+ # Found by their wiring, not their chapters: a `.hecksagon` is what says "this
70
+ # is a domain, and here is how its aggregates are stored", while most
71
+ # `.bluebook` files (era translations, the language's own grammar,
72
+ # `spec/fixtures`) are not domains anybody boots.
73
+ #
74
+ # @param root [String] the directory to search under
75
+ # @return [Array<String>] domain directory paths relative to `root`,
76
+ # deduplicated and sorted
77
+ def domains(root)
78
+ Dir.glob(File.join(root, "**/*.hecksagon"))
79
+ .map { |path| File.basename(File.dirname(path)) == "bluebook" ? File.dirname(path, 2) : File.dirname(path) }
80
+ .map { |path| path.delete_prefix("#{root}/") }
81
+ .grep_v(IGNORED)
82
+ .uniq.sort
83
+ end
84
+
85
+ # @api private
86
+ def target(argv, here)
87
+ if argv.empty? then [here, nil]
88
+ elsif argv.length > 1 then [argv[0], argv[1]]
89
+ elsif File.directory?(argv[0]) then [argv[0], nil]
90
+ else [here, argv[0]]
91
+ end
92
+ end
93
+
94
+ # @api private
95
+ def refuse_without_domain(program, root)
96
+ warn "usage: #{program} [domain-path] [aggregate]"
97
+ warn ""
98
+ warn "No bluebook here — #{Dir.pwd} is not inside a domain. Name one:"
99
+ domains(root).each { |candidate| warn " #{candidate}" }
100
+ exit 1
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,47 @@
1
+ require_relative "../../hecks"
2
+ # ADR 0033 — a domain wired to PostgresEra needs this plugin loaded
3
+ # explicitly; the era/lineage subsystem does not load with core.
4
+ require_relative "../ports/persistence/plugins/era"
5
+
6
+ module Hecks
7
+ module CLI
8
+ # The command behind `bin/ir` and `hecks ir`: a booted domain's IR as JSON, the
9
+ # same `to_h` the golden specs pin and `StorageShape` hashes into an era, for
10
+ # reading rather than asserting on.
11
+ module Ir
12
+ module_function
13
+
14
+ # Prints the IR `argv` asks for.
15
+ #
16
+ # `--meta` prints the language's own IR, the chapters under
17
+ # `lib/hecks/language/bluebook/` merged into one. It is reached through
18
+ # `Bluebook::MetaValidator.grammar_registry`, the real boot sequence, so it
19
+ # cannot drift from what `MetaValidator` itself loads.
20
+ #
21
+ # @param argv [Array<String>] a domain path and `--translations`, or `--meta`
22
+ # @param program [String] the name the usage message calls this command by
23
+ # @param default_domain [String, nil] the domain booted when `argv` names none;
24
+ # nil makes a domain argument required
25
+ # @return [void]
26
+ # @raise [SystemExit] when no domain is named and there is no default
27
+ def call(argv, program:, default_domain: nil)
28
+ argv = argv.dup
29
+ if argv.delete("--meta")
30
+ require_relative "../bluebook/meta_validator"
31
+ puts Projector::Exporter.json(Bluebook::MetaValidator.grammar_registry)
32
+ return
33
+ end
34
+
35
+ translations_only = argv.delete("--translations")
36
+ domain = argv.shift || default_domain or abort "usage: #{program} <domain> [--translations] | #{program} --meta"
37
+ runtime = Hecks.boot(domain)
38
+
39
+ if translations_only
40
+ puts Projector::Exporter.translations_json(runtime.registry)
41
+ else
42
+ puts Projector::Exporter.json(runtime.registry)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end