hecks 1.4.0 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (275) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +65 -0
  3. data/lib/hecks/adapters/driven/folder.rb +73 -0
  4. data/lib/hecks/adapters/driven/google_authentication.rb +25 -4
  5. data/lib/hecks/adapters/driven/governance_authorization.rb +57 -6
  6. data/lib/hecks/adapters/driven/heki/journal.rb +8 -0
  7. data/lib/hecks/adapters/driven/heki/saga_store.rb +53 -7
  8. data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
  9. data/lib/hecks/adapters/driven/heki.rb +93 -2
  10. data/lib/hecks/adapters/driven/identity_registry.rb +10 -0
  11. data/lib/hecks/adapters/driven/in_memory_ordering.rb +22 -0
  12. data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
  13. data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
  14. data/lib/hecks/adapters/driven/lambda/client.rb +35 -7
  15. data/lib/hecks/adapters/driven/lambda.rb +43 -2
  16. data/lib/hecks/adapters/driven/local_storage.rb +67 -1
  17. data/lib/hecks/adapters/driven/memory.rb +13 -13
  18. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +20 -0
  19. data/lib/hecks/adapters/driven/prism.rb +46 -7
  20. data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
  21. data/lib/hecks/adapters/driven/system_clock.rb +3 -0
  22. data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
  23. data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
  24. data/lib/hecks/adapters/driven.rb +2 -0
  25. data/lib/hecks/adapters/driving/github_webhook.rb +14 -1
  26. data/lib/hecks/behaviors/dsl.rb +58 -0
  27. data/lib/hecks/behaviors/expectations.rb +168 -7
  28. data/lib/hecks/behaviors/ir.rb +11 -0
  29. data/lib/hecks/behaviors/rspec.rb +8 -0
  30. data/lib/hecks/behaviors/runner.rb +19 -0
  31. data/lib/hecks/behaviors.rb +8 -0
  32. data/lib/hecks/bluebook/aggregate.rb +30 -0
  33. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +7 -0
  34. data/lib/hecks/bluebook/assembly/build.rb +25 -4
  35. data/lib/hecks/bluebook/assembly/contract.rb +66 -14
  36. data/lib/hecks/bluebook/assembly/contracts.rb +24 -19
  37. data/lib/hecks/bluebook/assembly/marks.rb +143 -14
  38. data/lib/hecks/bluebook/assembly/specializer.rb +19 -0
  39. data/lib/hecks/bluebook/assembly.rb +25 -7
  40. data/lib/hecks/bluebook/attribute.rb +17 -3
  41. data/lib/hecks/bluebook/behaviour/aggregate.rb +32 -0
  42. data/lib/hecks/bluebook/behaviour/attribute.rb +13 -0
  43. data/lib/hecks/bluebook/behaviour/chapter.rb +55 -2
  44. data/lib/hecks/bluebook/behaviour/command.rb +33 -3
  45. data/lib/hecks/bluebook/behaviour/domain_port.rb +20 -0
  46. data/lib/hecks/bluebook/behaviour/entity.rb +12 -0
  47. data/lib/hecks/bluebook/behaviour/hexagon.rb +26 -0
  48. data/lib/hecks/bluebook/behaviour/lifecycle.rb +23 -2
  49. data/lib/hecks/bluebook/behaviour/policy.rb +34 -9
  50. data/lib/hecks/bluebook/behaviour/process_manager.rb +32 -1
  51. data/lib/hecks/bluebook/behaviour/query.rb +5 -0
  52. data/lib/hecks/bluebook/behaviour/read_model.rb +21 -0
  53. data/lib/hecks/bluebook/behaviour/traits.rb +36 -0
  54. data/lib/hecks/bluebook/behaviour/value_object.rb +17 -5
  55. data/lib/hecks/bluebook/chapter.rb +23 -0
  56. data/lib/hecks/bluebook/command.rb +53 -8
  57. data/lib/hecks/bluebook/domain_port.rb +25 -0
  58. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +196 -37
  59. data/lib/hecks/bluebook/dsl/attribute_collector.rb +79 -42
  60. data/lib/hecks/bluebook/dsl/binding_proxy.rb +59 -0
  61. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +385 -53
  62. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +135 -21
  63. data/lib/hecks/bluebook/dsl/command_builder.rb +189 -27
  64. data/lib/hecks/bluebook/dsl/entity_builder.rb +139 -9
  65. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +41 -0
  66. data/lib/hecks/bluebook/dsl/identity_declaration.rb +23 -6
  67. data/lib/hecks/bluebook/dsl/policy_builder.rb +60 -19
  68. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +79 -15
  69. data/lib/hecks/bluebook/dsl/query_builder.rb +33 -4
  70. data/lib/hecks/bluebook/dsl/read_model_builder.rb +102 -27
  71. data/lib/hecks/bluebook/dsl/rule_reference.rb +60 -8
  72. data/lib/hecks/bluebook/dsl/translation_builder.rb +146 -40
  73. data/lib/hecks/bluebook/dsl/value_object_builder.rb +55 -7
  74. data/lib/hecks/bluebook/entity.rb +29 -0
  75. data/lib/hecks/bluebook/expression/ast_json.rb +115 -23
  76. data/lib/hecks/bluebook/expression/ast_reader.rb +29 -0
  77. data/lib/hecks/bluebook/expression/canonical_form.rb +48 -9
  78. data/lib/hecks/bluebook/expression/evaluator.rb +207 -29
  79. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +36 -0
  80. data/lib/hecks/bluebook/expression/resolver.rb +320 -77
  81. data/lib/hecks/bluebook/hexagon.rb +34 -0
  82. data/lib/hecks/bluebook/lifecycle.rb +11 -0
  83. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -0
  84. data/lib/hecks/bluebook/meta_validator/judge.rb +30 -26
  85. data/lib/hecks/bluebook/meta_validator/plan.rb +43 -8
  86. data/lib/hecks/bluebook/meta_validator/port_judge.rb +1 -0
  87. data/lib/hecks/bluebook/meta_validator/readings.rb +157 -7
  88. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +24 -4
  89. data/lib/hecks/bluebook/meta_validator/shapes.rb +141 -0
  90. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +149 -23
  91. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +5 -4
  92. data/lib/hecks/bluebook/meta_validator/world_judge.rb +1 -0
  93. data/lib/hecks/bluebook/meta_validator.rb +180 -84
  94. data/lib/hecks/bluebook/model_check.rb +268 -24
  95. data/lib/hecks/bluebook/pattern_subset.rb +23 -1
  96. data/lib/hecks/bluebook/process_manager.rb +13 -0
  97. data/lib/hecks/bluebook/project_discovery.rb +5 -0
  98. data/lib/hecks/bluebook/project_loader.rb +40 -0
  99. data/lib/hecks/bluebook/project_register.rb +44 -0
  100. data/lib/hecks/bluebook/query.rb +27 -0
  101. data/lib/hecks/bluebook/read_model.rb +21 -1
  102. data/lib/hecks/bluebook/reference.rb +21 -8
  103. data/lib/hecks/bluebook/smoke_test.rb +29 -6
  104. data/lib/hecks/bluebook/synthesizer.rb +34 -0
  105. data/lib/hecks/bluebook/translation.rb +30 -1
  106. data/lib/hecks/bluebook/value_object.rb +23 -5
  107. data/lib/hecks/bluebook.rb +3 -4
  108. data/lib/hecks/codemod.rb +107 -20
  109. data/lib/hecks/construct.rb +15 -1
  110. data/lib/hecks/corpus.rb +146 -25
  111. data/lib/hecks/deploy/bluebook/deploy.bluebook +105 -0
  112. data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
  113. data/lib/hecks/deploy/oidc.json +5 -0
  114. data/lib/hecks/doc/reference.rb +185 -16
  115. data/lib/hecks/embryonaut_bluebook.rb +32 -9
  116. data/lib/hecks/facade/handle.rb +76 -3
  117. data/lib/hecks/facade/surface/aggregate_door.rb +8 -0
  118. data/lib/hecks/forms/field_shape.rb +3 -0
  119. data/lib/hecks/forms/page.rb +14 -0
  120. data/lib/hecks/forms/port_argument.rb +12 -0
  121. data/lib/hecks/forms/query_form_renderer.rb +63 -0
  122. data/lib/hecks/forms/record_renderer.rb +58 -0
  123. data/lib/hecks/forms/record_table.rb +27 -0
  124. data/lib/hecks/forms/reference_options.rb +24 -0
  125. data/lib/hecks/forms/value_object_shape.rb +10 -0
  126. data/lib/hecks/fqn.rb +58 -0
  127. data/lib/hecks/framework/bluebook/compliance.bluebook +221 -0
  128. data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
  129. data/lib/hecks/framework/oidc.json +15 -0
  130. data/lib/hecks/framework.rb +43 -20
  131. data/lib/hecks/freezer.rb +17 -1
  132. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +159 -24
  133. data/lib/hecks/fuzzing/combination_miner.rb +59 -0
  134. data/lib/hecks/fuzzing/concurrent_dispatch.rb +109 -8
  135. data/lib/hecks/fuzzing/coverage_campaign.rb +56 -13
  136. data/lib/hecks/fuzzing/differential.rb +34 -0
  137. data/lib/hecks/fuzzing/domain_generator.rb +188 -11
  138. data/lib/hecks/fuzzing/era_boundary.rb +45 -15
  139. data/lib/hecks/fuzzing/form_census.rb +86 -0
  140. data/lib/hecks/fuzzing/generated_domain_check.rb +76 -0
  141. data/lib/hecks/fuzzing/invalid_value_generator.rb +39 -0
  142. data/lib/hecks/fuzzing/isolated_boot.rb +79 -22
  143. data/lib/hecks/fuzzing/nondeterministic.rb +13 -1
  144. data/lib/hecks/fuzzing/persistence_parity.rb +95 -3
  145. data/lib/hecks/fuzzing/properties/corrections.rb +25 -0
  146. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +158 -14
  147. data/lib/hecks/fuzzing/properties/guards.rb +44 -0
  148. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +48 -0
  149. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +18 -0
  150. data/lib/hecks/fuzzing/properties/outbox.rb +49 -11
  151. data/lib/hecks/fuzzing/properties/querying.rb +68 -14
  152. data/lib/hecks/fuzzing/properties.rb +24 -15
  153. data/lib/hecks/fuzzing/qa_settings.rb +12 -0
  154. data/lib/hecks/fuzzing/replay.rb +137 -29
  155. data/lib/hecks/fuzzing/rotation_priority.rb +41 -21
  156. data/lib/hecks/fuzzing/rust_gap_manifest.rb +46 -20
  157. data/lib/hecks/fuzzing/self_consistency.rb +189 -40
  158. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +12 -6
  159. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +1 -1
  160. data/lib/hecks/fuzzing/sequence_generator.rb +47 -0
  161. data/lib/hecks/fuzzing/shrinker.rb +114 -2
  162. data/lib/hecks/fuzzing/structural_skips.rb +18 -4
  163. data/lib/hecks/fuzzing/sweep_depth.rb +8 -0
  164. data/lib/hecks/fuzzing/target_capabilities.rb +61 -14
  165. data/lib/hecks/fuzzing/value_generator.rb +98 -10
  166. data/lib/hecks/grammar/evolve.rb +178 -2
  167. data/lib/hecks/grammar.rb +46 -0
  168. data/lib/hecks/ir.rb +38 -7
  169. data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
  170. data/lib/hecks/literal.rb +32 -0
  171. data/lib/hecks/naming.rb +88 -7
  172. data/lib/hecks/ports/access_control.rb +5 -10
  173. data/lib/hecks/ports/authorization.rb +3 -6
  174. data/lib/hecks/ports/identity_assignment.rb +1 -2
  175. data/lib/hecks/ports/identity_resolution.rb +1 -2
  176. data/lib/hecks/ports/key_vault.port +6 -0
  177. data/lib/hecks/ports/key_vault.rb +85 -0
  178. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +6 -0
  179. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +38 -2
  180. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -0
  181. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +40 -0
  182. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +92 -1
  183. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
  184. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +8 -0
  185. data/lib/hecks/ports/query/in_memory.rb +39 -6
  186. data/lib/hecks/ports/query/ordering.rb +15 -0
  187. data/lib/hecks/ports.rb +1 -0
  188. data/lib/hecks/projections/bootstrap_table.rb +43 -8
  189. data/lib/hecks/projections/deploy/fargate.rb +666 -0
  190. data/lib/hecks/projections/deploy/lambda.rb +2423 -0
  191. data/lib/hecks/projections/deploy/shared.rb +624 -0
  192. data/lib/hecks/projections/diagrams.rb +243 -7
  193. data/lib/hecks/projections/glossary/html.rb +88 -0
  194. data/lib/hecks/projections/glossary/markdown.rb +47 -0
  195. data/lib/hecks/projections/glossary/mermaid.rb +48 -0
  196. data/lib/hecks/projections/glossary/sentences.rb +105 -0
  197. data/lib/hecks/projections/glossary.rb +161 -19
  198. data/lib/hecks/projections/model/deviations.rb +44 -0
  199. data/lib/hecks/projections/model.rb +51 -1
  200. data/lib/hecks/projections/oidc.rb +40 -3
  201. data/lib/hecks/projections/parser_table.rb +49 -8
  202. data/lib/hecks/projections/reference.rb +12 -0
  203. data/lib/hecks/projections/rust_vocabulary.rb +219 -16
  204. data/lib/hecks/projections/shape.rb +8 -0
  205. data/lib/hecks/projections/statements.rb +63 -16
  206. data/lib/hecks/projections/vocabulary.rb +17 -0
  207. data/lib/hecks/projections.rb +3 -0
  208. data/lib/hecks/projector/cli_projector.rb +218 -10
  209. data/lib/hecks/projector/docs_projector.rb +145 -19
  210. data/lib/hecks/projector/exporter.rb +65 -11
  211. data/lib/hecks/projector/ir_projector.rb +6 -0
  212. data/lib/hecks/projector/narrate_projector.rb +136 -15
  213. data/lib/hecks/projector/target.rb +47 -10
  214. data/lib/hecks/projector.rb +101 -19
  215. data/lib/hecks/query_ir.rb +47 -0
  216. data/lib/hecks/query_specification/common/null_policy.rb +5 -3
  217. data/lib/hecks/rendering.rb +6 -0
  218. data/lib/hecks/router/namespace_installer.rb +13 -0
  219. data/lib/hecks/router.rb +55 -0
  220. data/lib/hecks/runtime/aggregate_lock.rb +9 -0
  221. data/lib/hecks/runtime/boot_gates.rb +18 -0
  222. data/lib/hecks/runtime/caller.rb +32 -0
  223. data/lib/hecks/runtime/capability_graph.rb +11 -0
  224. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +23 -21
  225. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +14 -15
  226. data/lib/hecks/runtime/command_interpreter.rb +42 -17
  227. data/lib/hecks/runtime/command_rules/admissibility.rb +165 -14
  228. data/lib/hecks/runtime/command_rules/arithmetic.rb +17 -5
  229. data/lib/hecks/runtime/command_rules/references.rb +118 -28
  230. data/lib/hecks/runtime/dependency_planning.rb +45 -0
  231. data/lib/hecks/runtime/dispatcher.rb +28 -50
  232. data/lib/hecks/runtime/entity_element.rb +161 -8
  233. data/lib/hecks/runtime/entity_interpreter.rb +44 -9
  234. data/lib/hecks/runtime/errors.rb +18 -4
  235. data/lib/hecks/runtime/event.rb +10 -5
  236. data/lib/hecks/runtime/identity.rb +71 -3
  237. data/lib/hecks/runtime/instance.rb +67 -7
  238. data/lib/hecks/runtime/interpreting.rb +13 -5
  239. data/lib/hecks/runtime/invocation.rb +118 -36
  240. data/lib/hecks/runtime/loader.rb +94 -8
  241. data/lib/hecks/runtime/outbox.rb +145 -7
  242. data/lib/hecks/runtime/policy_interpreter.rb +22 -9
  243. data/lib/hecks/runtime/port_operation_interpreter.rb +20 -0
  244. data/lib/hecks/runtime/query_interpreter.rb +40 -12
  245. data/lib/hecks/runtime/reaction_invocation.rb +53 -8
  246. data/lib/hecks/runtime/read_model_interpreter.rb +23 -7
  247. data/lib/hecks/runtime/rebuild_sweep.rb +28 -0
  248. data/lib/hecks/runtime/reference_hop.rb +42 -0
  249. data/lib/hecks/runtime/refusal_wording.rb +50 -0
  250. data/lib/hecks/runtime/registry/saga_persistence.rb +11 -0
  251. data/lib/hecks/runtime/registry/verification.rb +119 -4
  252. data/lib/hecks/runtime/registry.rb +157 -4
  253. data/lib/hecks/runtime/remote_dispatcher.rb +92 -6
  254. data/lib/hecks/runtime/routing.rb +27 -2
  255. data/lib/hecks/runtime/saga_interpreter/correlation.rb +10 -12
  256. data/lib/hecks/runtime/saga_interpreter.rb +27 -13
  257. data/lib/hecks/runtime/tenant_check.rb +26 -6
  258. data/lib/hecks/runtime/tenant_scope.rb +18 -0
  259. data/lib/hecks/runtime/value/coercion.rb +255 -33
  260. data/lib/hecks/runtime/value/entity_list_coercion.rb +102 -30
  261. data/lib/hecks/runtime/value.rb +50 -0
  262. data/lib/hecks/runtime.rb +32 -0
  263. data/lib/hecks/storehouse.rb +305 -9
  264. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
  265. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
  266. data/lib/hecks/version.rb +1 -1
  267. data/lib/hecks.rb +79 -1
  268. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -0
  269. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +12 -2
  270. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +29 -5
  271. metadata +14 -5
  272. data/lib/hecks/codemod/legacy_dispatch_args.rb +0 -299
  273. data/lib/hecks/codemod/legacy_dispatch_recorder.rb +0 -186
  274. data/lib/hecks/deprecation.rb +0 -95
  275. data/lib/hecks/framework/bluebook/compliance.bluebook +0 -1
@@ -14,18 +14,33 @@ module Hecks
14
14
  include QuerySpecification::Common::DSL
15
15
  include WordGate
16
16
 
17
+ # @param name [String] the read model's own name, as written after `read_model`
17
18
  def initialize(name)
18
19
  @name = name
19
20
  end
20
21
 
22
+ # Sets the human-readable description shown for this read model.
23
+ #
24
+ # @param value [String] the description text
25
+ # @return [String] the description as stored
21
26
  def description(value)
22
27
  # moved to the language: ProjectionText / purpose, on Projection.Declare
23
28
  @description = value
24
29
  end
25
30
 
26
- # Renamed from `reference_to` — item #13's full metaprogrammed
27
- # dispatch (slice 4b). Bootstrap-reachable, in
28
- # GenericDispatch::BOOTSTRAP_CALLS_FALLBACK.
31
+ # Declares the read model's own "one" side — the aggregate head every row projects
32
+ # around, keyed by its identity.
33
+ #
34
+ # Reached through `calls: "reference_to_impl"`. Bootstrap-reachable — the self-hosted
35
+ # `Bluebook.WholeBluebook` read model declares one — so it's exercised while
36
+ # `GenericDispatch::BOOTSTRAP_CALLS_FALLBACK` is still in play, not only afterwards.
37
+ #
38
+ # @param type [Module, Symbol, String] the referenced aggregate, written as a bare
39
+ # constant
40
+ # @param as [Symbol, nil] the field name to project the reference under; `nil` derives it
41
+ # from the target's own name
42
+ # @return [void]
43
+ # @raise [Bluebook::DSL::Malformed] if `reference_to` was already declared
29
44
  def reference_to_impl(type, as: nil)
30
45
  raise Malformed, "#{@name} already has a projection reference" if @reference_target
31
46
 
@@ -33,22 +48,28 @@ module Hecks
33
48
  @reference_name = (as || Naming.snake(@reference_target)).to_sym
34
49
  end
35
50
 
36
- # Order-independent. `many:` is decided by comparing the included type
37
- # against the reference target, so this used to refuse an include
38
- # declared before the reference — a rule guarding an implementation
39
- # limitation rather than a truth about read models. The includes are
40
- # collected raw and resolved at build, when the reference is known, so
41
- # there is no rule left to enforce.
42
- # Renamed from `include`/`group_by` — item #13's full
43
- # metaprogrammed dispatch (slice 4c). `include` is bootstrap-
44
- # reachable (every core chapter's own `read_model` names which
45
- # aggregates it includes with it — a first grep dismissed this
46
- # as `Module#include` noise and was wrong; the cold-boot test
47
- # after this rename caught it directly), so it's in
48
- # BOOTSTRAP_CALLS_FALLBACK; `group_by` is not (no core read_model
49
- # groups). The class-level `include WordGate` this file's own
50
- # class body uses is `Module#include`, a different receiver,
51
- # unaffected by renaming this instance method either way.
51
+ # Adds one aggregate head this read model projects.
52
+ #
53
+ # Order-independent: `many:` is decided by comparing the included type
54
+ # against the reference target, resolved at build time (when the
55
+ # reference is known), not at the moment `include` itself runs — so
56
+ # an include declared before `reference_to` resolves exactly like one
57
+ # declared after it.
58
+ #
59
+ # Reached through `calls: "include_impl"`. Bootstrap-reachable —
60
+ # every core chapter's own `read_model` names which aggregates it
61
+ # includes with it — so it's exercised while
62
+ # `GenericDispatch::BOOTSTRAP_CALLS_FALLBACK` is still in play.
63
+ # `group_by_impl`, below, is table-routed the same way but is not
64
+ # bootstrap-reachable (no core read_model groups). The class-level
65
+ # `include WordGate` this file's own class body uses is
66
+ # `Module#include`, a different receiver, unrelated to this
67
+ # instance method sharing its name.
68
+ #
69
+ # @param type [Module, Symbol, String] the included aggregate, written as a bare constant
70
+ # @param as [Symbol, nil] the field name to project this head's rows under; `nil` derives
71
+ # it from the target's own name (pluralized for a many-side head)
72
+ # @return [void]
52
73
  def include_impl(type, as: nil)
53
74
  @includes ||= []
54
75
  @includes << [Naming.demodulise(type), as]
@@ -96,6 +117,21 @@ module Hecks
96
117
  # not this `Struct`) — found directly by reproducing "undefined
97
118
  # method `new' for module WhereClause" against a real corpus load,
98
119
  # not guessed.
120
+ #
121
+ # Records one `WhereClause` per `field => value` pair, targeted at a named many-side
122
+ # include when this read model has more than one.
123
+ #
124
+ # @param positional [Array<Hash{Symbol => Object}>] at most one bare Hash of where
125
+ # clauses; in practice always empty, since Ruby routes an ordinary
126
+ # `where(field: value)` call entirely into `rest` (see the comment above)
127
+ # @param on [Module, Symbol, String, nil] the many-side included aggregate these clauses
128
+ # apply to; `nil` when this read model has at most one many-side head
129
+ # @param rest [Hash{Symbol => Object}] `field => value` where clauses; a value is either
130
+ # a literal, a Symbol naming a query argument, or a one-pair Hash `{ comparator =>
131
+ # operand }` such as `{ gte: :minimum }` — a bare value means `eq`
132
+ # @return [void]
133
+ # @raise [ArgumentError] if more than one positional argument is given, or a Hash value
134
+ # does not have exactly one pair, or names a comparator outside `COMPARATORS`
99
135
  def where_impl(*positional, on: nil, **rest)
100
136
  raise ArgumentError, "wrong number of arguments (given #{positional.size}, expected 1)" if positional.size > 1
101
137
 
@@ -108,14 +144,36 @@ module Hecks
108
144
  end
109
145
  end
110
146
 
147
+ # Records the read model's single ordering, replacing any declared earlier.
148
+ #
149
+ # @param field [Symbol, String] the field to order by; a dotted path such as
150
+ # `:"order.value"` reaches a value object's member
151
+ # @param direction [Symbol, String] `:asc` or `:desc`
152
+ # @param on [Module, Symbol, String, nil] the many-side included aggregate this ordering
153
+ # applies to; `nil` when this read model has at most one many-side head
154
+ # @return [QuerySpecification::Common::OrderBy] the ordering just recorded
111
155
  def order_by_impl(field, direction = :asc, on: nil)
112
156
  @order_by = QuerySpecification::Common::OrderBy.new(field: field, direction: direction, target: resolve_target(on))
113
157
  end
114
158
 
159
+ # Records the most rows the targeted collection returns.
160
+ #
161
+ # @param value [Integer, Symbol] a literal row count, or a Symbol naming the query
162
+ # argument that supplies it
163
+ # @param on [Module, Symbol, String, nil] the many-side included aggregate this limit
164
+ # applies to; `nil` when this read model has at most one many-side head
165
+ # @return [QuerySpecification::Common::LimitSpec] the limit just recorded
115
166
  def limit_impl(value, on: nil)
116
167
  @limit = QuerySpecification::Common::LimitSpec.new(value: value, target: resolve_target(on))
117
168
  end
118
169
 
170
+ # Records how many matched rows the targeted collection skips before the limit applies.
171
+ #
172
+ # @param value [Integer, Symbol] a literal row count, or a Symbol naming the query
173
+ # argument that supplies it
174
+ # @param on [Module, Symbol, String, nil] the many-side included aggregate this offset
175
+ # applies to; `nil` when this read model has at most one many-side head
176
+ # @return [QuerySpecification::Common::OffsetSpec] the offset just recorded
119
177
  def offset_impl(value, on: nil)
120
178
  @offset = QuerySpecification::Common::OffsetSpec.new(value: value, target: resolve_target(on))
121
179
  end
@@ -127,6 +185,9 @@ module Hecks
127
185
  # `seal_query_options` already enforces for where/order_by/etc
128
186
  # applies here too (`seal_group_by`) — grouping is a question
129
187
  # about one collection's own rows, same as those are.
188
+ # @param fields [Array<Symbol>] the eligible many-side head's own fields to nest its rows
189
+ # under, one level per field
190
+ # @return [void]
130
191
  def group_by_impl(*fields)
131
192
  # Hash rows, `{field:}`, not bare symbols — same shape
132
193
  # `aggregate_heads` already uses for exactly the reason it
@@ -168,16 +229,24 @@ module Hecks
168
229
  # same shape as `count`, above (a bare, kind-driven coerce-and-
169
230
  # assign).
170
231
 
171
- # `reference_to` is now optional — a read model with no root is a
232
+ # Assembles the declared references, includes and clauses into a `ReadModel`, after
233
+ # validating them.
234
+ #
235
+ # `reference_to` is optional — a read model with no root is a
172
236
  # bulk one: every `include`d head reads its own aggregate whole
173
237
  # (no FK match against a root that doesn't exist), and dispatch
174
- # takes no id argument at all. This used to be required, on the
175
- # assumption a read model was always "one root record's own
176
- # cross-aggregate view" — true of every real corpus report so
177
- # far, but not a truth about read models themselves: `group_by`'s
178
- # own real use (nesting an aggregate's own whole table by its own
179
- # field values) has no root to speak of. Still needs to describe
180
- # something — zero includes and no reference is refused.
238
+ # takes no id argument at all. Requiring one would refuse a real,
239
+ # legitimate shape: `group_by`'s own real use (nesting an
240
+ # aggregate's own whole table by its own field values) has no root
241
+ # to speak of. Still needs to describe something — zero includes
242
+ # and no reference is refused.
243
+ #
244
+ # @return [Bluebook::ReadModel] the built read model
245
+ # @raise [Bluebook::DSL::Malformed] if neither `reference_to` nor any `include` is
246
+ # declared, if `where`/`order_by`/`limit`/`offset`/`group_by`/`count`/`median` name an
247
+ # `on:` that isn't a many-side include or are left untargeted with more than one
248
+ # many-side head, if `count` and `median` are both declared or combined with
249
+ # `group_by`, or if `cursor` is declared
181
250
  def build
182
251
  if !@reference_target && Array(@includes).empty?
183
252
  raise Malformed,
@@ -200,6 +269,12 @@ module Hecks
200
269
  count: @count, median_field: @median_field)
201
270
  end
202
271
 
272
+ # Evaluates a `read_model` block against a fresh builder and returns what it built.
273
+ #
274
+ # @param name [String] the read model's own name
275
+ # @yield the read model's body, `instance_eval`'d against a new builder
276
+ # @return [Bluebook::ReadModel] the built read model
277
+ # @raise [Bluebook::DSL::Malformed] see `#build`
203
278
  def self.build(name, &block)
204
279
  builder = new(name)
205
280
  builder.instance_eval(&block) if block
@@ -15,9 +15,10 @@ module Hecks
15
15
  # reuses one of these three shapes instead of a fourth hand-written
16
16
  # near-duplicate resolver.
17
17
  #
18
- # **Not one unified algorithm** — a real design question this file
19
- # answers directly: the three existing resolvers are not
20
- # superficially different, they are structurally different (a
18
+ # ## Not one unified algorithm
19
+ #
20
+ # A real design question this file answers directly: the three existing
21
+ # resolvers are not superficially different, they are structurally different (a
21
22
  # multi-pool fallback chain; one pool keyed by declaring owner,
22
23
  # needing disambiguation; a live scan over already-built sibling
23
24
  # objects with no separate pool at all) — forcing them into one
@@ -28,18 +29,21 @@ module Hecks
28
29
  # `invariant`×3, `ensures`×1) before this file existed — extract
29
30
  # predicate source, refuse if extraction failed, build the struct.
30
31
  #
32
+ # ## Resolution rules read off the grammar table, not a Ruby-only mirror
33
+ #
31
34
  # `#lookup`/`#verify_resolves_via!` read which construct uses which
32
35
  # primitive off the self-hosted grammar table itself
33
- # (`Keyword#resolves_via`, `syntax.bluebook`) — not a Ruby-only
34
- # Hash cross-checked afterward (this file's own earlier shape, one
35
- # round ago) — so a real domain's own boot, not just `bundle exec
36
- # rspec`, fails loudly the moment the table and this file's own
36
+ # (`Keyword#resolves_via`, `syntax.bluebook`), so a real domain's own boot, not just
37
+ # `bundle exec rspec`, fails loudly the moment the table and this file's own
37
38
  # hand-written resolution methods disagree.
38
39
  module RuleReference
39
40
  module_function
40
41
 
42
+ # Extracts a predicate block's source and builds the rule struct that holds it,
43
+ # refusing when the source could not be read.
44
+ #
41
45
  # `struct_class` is `Given` or `Invariant` (both `Struct.new(
42
- # :description, :canonical, :predicate, keyword_init: true)` —
46
+ # :description, :canonical, :predicate, :ast, keyword_init: true)` —
43
47
  # `Given` lives in command.rb, `Invariant` in value_object.rb).
44
48
  # `owner_name`/`word` are only for the refusal message's own
45
49
  # wording. `extraction_failure` is the tail of that same
@@ -52,6 +56,22 @@ module Hecks
52
56
  # this method existed; unifying them into one generic sentence
53
57
  # would be a real (if small) behavior change this refactor is
54
58
  # not making.
59
+ #
60
+ # @param struct_class [Class] the rule struct to build — `Bluebook::Given` or
61
+ # `Bluebook::Invariant`
62
+ # @param description [String] the rule's own description, as declared by the caller's
63
+ # own `given`/`invariant`/`ensures` word
64
+ # @param predicate [Proc] the rule's own body block, never called here — only its
65
+ # extracted source is used
66
+ # @param owner_name [String] the declaring construct's own name, for the refusal message
67
+ # @param word [String] the declaring word (`"given"`, `"invariant"`, or `"ensures"`),
68
+ # for the refusal message
69
+ # @param extraction_failure [String] the refusal message's own tail, naming what an
70
+ # unreadable predicate would mean for this particular word
71
+ # @return [Bluebook::Given, Bluebook::Invariant] the built rule struct, an instance of
72
+ # `struct_class`
73
+ # @raise [Bluebook::DSL::Malformed] if `predicate`'s source could not be extracted, or
74
+ # it matches against a pattern construct `Expression::AstJson::PatternSubset` refuses
55
75
  def build_rule(struct_class, description, predicate, owner_name:, word:, extraction_failure:)
56
76
  canonical = Ports::Extraction.canonical(predicate)
57
77
 
@@ -72,6 +92,11 @@ module Hecks
72
92
  # piece's entity-wide pool) is this with a 2-element chain — a
73
93
  # future single-pool bare reference is the same primitive with a
74
94
  # 1-element chain, not a separate "just look in one hash" method.
95
+ # @param pools [Array<Hash{String => Bluebook::Given, Bluebook::Invariant}>] pools to
96
+ # search in order; the first pool holding `description` wins
97
+ # @param description [String] the rule's own description to find
98
+ # @return [Bluebook::Given, Bluebook::Invariant, nil] the matching rule, or `nil` if no
99
+ # pool has one
75
100
  def resolve_hash_chain(pools, description)
76
101
  pools.each { |pool| return pool[description] if pool.key?(description) }
77
102
  nil
@@ -87,6 +112,11 @@ module Hecks
87
112
  # wording for "none," "ambiguous," and "declared_by: named the
88
113
  # wrong owner" rather than one generic message papering over all
89
114
  # three.
115
+ # @param pool [Hash{String => Hash{String => Bluebook::Given, Bluebook::Invariant}}]
116
+ # descriptions mapped to their own candidates, each keyed by declaring owner
117
+ # @param description [String] the rule's own description to find
118
+ # @return [Hash{String => Bluebook::Given, Bluebook::Invariant}] the candidates for
119
+ # `description`, keyed by declaring owner; empty when none exist
90
120
  def resolve_owner_keyed(pool, description)
91
121
  pool[description] || {}
92
122
  end
@@ -102,6 +132,13 @@ module Hecks
102
132
  # sibling (`:invariants` today; kept a parameter, not hardcoded,
103
133
  # since a future sibling-scan scope might reference a different
104
134
  # collection).
135
+ # @param siblings [Array<Object>] the already-built sibling objects to scan; each must
136
+ # respond to `reader`
137
+ # @param description [String] the rule's own description to find
138
+ # @param reader [Symbol] the method to call on each sibling to read its own rule
139
+ # collection, such as `:invariants`
140
+ # @return [Bluebook::Given, Bluebook::Invariant, nil] the matching rule, or `nil` if no
141
+ # sibling declares one
105
142
  def resolve_sibling_scan(siblings, description, reader:)
106
143
  siblings.flat_map { |sibling| sibling.public_send(reader) }
107
144
  .find { |rule| rule.description == description }
@@ -138,6 +175,14 @@ module Hecks
138
175
  # (bin/project_bootstrap_table, pinned by spec/bootstrap_table_spec.rb).
139
176
  BOOTSTRAP_FALLBACK = BootstrapTable::RESOLVES
140
177
 
178
+ # Reads how a (word, context) pair resolves its rule references, off the self-hosted
179
+ # grammar table itself (or, while that table is still booting, the projected fallback).
180
+ #
181
+ # @param word [String] the DSL word, such as `"given"` or `"invariant"`
182
+ # @param context [String] the grammar context, such as `"Aggregate"`
183
+ # @return [Hash{Symbol => String, nil}] `:resolves_via` and `:disambiguator` for this
184
+ # (word, context) pair, `nil`-valued when the row leaves either blank, or `{}` if no row
185
+ # matches at all
141
186
  def lookup(word, context)
142
187
  if MetaValidator.bootstrapping?
143
188
  BOOTSTRAP_FALLBACK[[word, context]] || {}
@@ -162,6 +207,13 @@ module Hecks
162
207
  # real drift between the language's own self-description and
163
208
  # its own implementation, caught at the next boot of anything,
164
209
  # not just the next `rspec` run.
210
+ # @param word [String] the DSL word, such as `"given"` or `"invariant"`
211
+ # @param context [String] the grammar context, such as `"Aggregate"`
212
+ # @param expected_primitive [String] the resolution primitive's own name the caller is
213
+ # about to use, such as `"hash_chain"`, `"owner_keyed"`, or `"sibling_scan"`
214
+ # @return [void]
215
+ # @raise [RuntimeError] if `syntax.bluebook`'s own `resolves_via` for this (word, context)
216
+ # pair names something other than `expected_primitive`
165
217
  def verify_resolves_via!(word, context, expected_primitive)
166
218
  actual = lookup(word, context)[:resolves_via]
167
219
  return if actual == expected_primitive
@@ -13,6 +13,9 @@ module Hecks
13
13
 
14
14
  include WordGate
15
15
 
16
+ # @param name [String, Symbol] the aggregate's name in the destination era
17
+ # @param was [String, Symbol, nil] the aggregate's earlier name, when renamed
18
+ # @raise [Bluebook::DSL::Malformed] if `name` is empty
16
19
  def initialize(name, was: nil)
17
20
  raise Malformed, "an aggregate translation needs a name" if name.to_s.empty?
18
21
 
@@ -28,14 +31,23 @@ module Hecks
28
31
  @backfills = []
29
32
  end
30
33
 
31
- # Renamed from `rename`/`move`/`convert`/`retype`/`compute`/
32
- # `rekey`/`backfill` (all seven below) — item #13's full
33
- # metaprogrammed dispatch (slice 4c). Not bootstrap-reachable
34
- # (translation.bluebook describes its own structure with
35
- # aggregate/entity/attribute, never with these — they're
36
- # words for real, user-authored `.translation` files only,
37
- # loaded after the grammar table exists), so none need a
38
- # BOOTSTRAP_CALLS_FALLBACK entry.
34
+ # Declares a field rename with no other change: same path, new name.
35
+ #
36
+ # Answers the `rename` word (and, via the same table rows, its
37
+ # siblings `move`/`convert`/`retype`/`compute`/`rekey`/`backfill`
38
+ # below) through the table's `calls:` column — item #13's full
39
+ # metaprogrammed dispatch (slice 4c). Each is carried in
40
+ # `GenericDispatch::BOOTSTRAP_CALLS_FALLBACK` like every other
41
+ # `calls:`-routed word, but none of their rows are ever consulted
42
+ # during real bootstrap: `translation.bluebook` describes its own
43
+ # structure with aggregate/entity/attribute, never with these —
44
+ # they're words for real, user-authored `.translation` files
45
+ # only, loaded after the grammar table already exists.
46
+ #
47
+ # @param old_name [Symbol, String] the field's name in the held era
48
+ # @param to [Symbol, String] the field's name in the destination era
49
+ # @return [void]
50
+ # @raise [Bluebook::DSL::Malformed] if `old_name` or `to` is empty
39
51
  def rename_impl(old_name, to:)
40
52
  raise Malformed, "a rename needs a source name" if old_name.to_s.empty?
41
53
  raise Malformed, "a rename needs a destination name (to:)" if to.to_s.empty?
@@ -43,6 +55,13 @@ module Hecks
43
55
  @renames[old_name.to_sym] = to.to_sym
44
56
  end
45
57
 
58
+ # Declares a field moved to a different path, name unchanged.
59
+ #
60
+ # @param old_path [String, Symbol] the field's path in the held era; dotted reaches a
61
+ # value-object member
62
+ # @param to [String, Symbol] the field's path in the destination era
63
+ # @return [Array<Bluebook::TranslationMove>] every move declared so far, this one last
64
+ # @raise [Bluebook::DSL::Malformed] if `old_path` or `to` is empty
46
65
  def move_impl(old_path, to:)
47
66
  raise Malformed, "a move needs a destination path (to:)" if to.to_s.empty?
48
67
  raise Malformed, "a move needs a source path" if old_path.to_s.empty?
@@ -50,10 +69,21 @@ module Hecks
50
69
  @moves << TranslationMove.new(old_path.to_s, to.to_s)
51
70
  end
52
71
 
72
+ # Declares an exhaustive value-to-value mapping for a field with nothing structural in
73
+ # common with its replacement.
74
+ #
53
75
  # A value with nothing structural in common with its replacement
54
76
  # — declared as an exhaustive table, not computed, so every value
55
77
  # that can appear in old data has a named destination. Paths
56
78
  # follow `move`'s convention: dotted reaches a value-object member.
79
+ #
80
+ # @param old_path [String, Symbol] the field's path in the held era
81
+ # @param to [String, Symbol] the field's path in the destination era
82
+ # @param values [Hash] every old value mapped to its destination value
83
+ # @return [Array<Bluebook::TranslationConvert>] every convert declared so far, this one
84
+ # last
85
+ # @raise [Bluebook::DSL::Malformed] if `old_path` or `to` is empty, or `values` is nil
86
+ # or empty
57
87
  def convert_impl(old_path, to:, values:)
58
88
  raise Malformed, "a convert needs a destination path (to:)" if to.to_s.empty?
59
89
  raise Malformed, "a convert needs a source path" if old_path.to_s.empty?
@@ -70,10 +100,18 @@ module Hecks
70
100
  # coerce-and-append with nothing else, now executed by
71
101
  # `GenericDispatch`.
72
102
 
103
+ # Declares that a type's name changed while its member structure stayed the same.
104
+ #
73
105
  # A value object's or entity's own type name changed, member
74
106
  # structure unchanged. The stored data never carries the type
75
107
  # name, so nothing moves — this declares that the pair of names
76
108
  # means the same shape, which is what lets the era diff accept it.
109
+ #
110
+ # @param old_type [String, Symbol] the type's name in the held era
111
+ # @param to [String, Symbol] the type's name in the destination era
112
+ # @return [Array<Bluebook::TranslationRetype>] every retype declared so far, this one
113
+ # last
114
+ # @raise [Bluebook::DSL::Malformed] if `old_type` or `to` is empty
77
115
  def retype_impl(old_type, to:)
78
116
  raise Malformed, "a retype needs a source type name" if old_type.to_s.empty?
79
117
  raise Malformed, "a retype needs a destination type name (to:)" if to.to_s.empty?
@@ -81,10 +119,19 @@ module Hecks
81
119
  @retypes << TranslationRetype.new(old_type.to_s, to.to_s)
82
120
  end
83
121
 
122
+ # Declares a field computed by a hand-written Postgres SQL expression.
123
+ #
84
124
  # A computed transform whose only implementation is the SQL
85
125
  # expression itself — Postgres-only by construction. The scaffold
86
126
  # never proposes one; a human writes it, and the audit's
87
127
  # human-sampled review is its only verification.
128
+ #
129
+ # @param old_path [String, Symbol] the source field's path in the held era
130
+ # @param to [String, Symbol] the field's path in the destination era
131
+ # @param sql [String] the Postgres SQL expression computing the destination value
132
+ # @return [Array<Bluebook::TranslationCompute>] every compute declared so far, this one
133
+ # last
134
+ # @raise [Bluebook::DSL::Malformed] if `old_path`, `to`, or `sql` is empty
88
135
  def compute_impl(old_path, to:, sql:)
89
136
  raise Malformed, "a compute needs a destination path (to:)" if to.to_s.empty?
90
137
  raise Malformed, "a compute needs a source path" if old_path.to_s.empty?
@@ -93,6 +140,9 @@ module Hecks
93
140
  @computes << TranslationCompute.new(old_path.to_s, to.to_s, sql.to_s)
94
141
  end
95
142
 
143
+ # Declares a hand-written Postgres SQL expression that recomputes the aggregate's own
144
+ # identity.
145
+ #
96
146
  # The aggregate's own identity, changing what it's computed from —
97
147
  # not a field crossing a boundary (`move`), not a value's own
98
148
  # transform (`compute`): the record's key. No path arguments,
@@ -101,12 +151,19 @@ module Hecks
101
151
  # SQL-only, Postgres-only, human-reviewed-sample-is-the-only-
102
152
  # verification shape `compute` already has, and for the same
103
153
  # reason: there is nothing in-process to check this against.
154
+ #
155
+ # @param sql [String] the Postgres SQL expression computing the destination identity
156
+ # @return [Array<Bluebook::TranslationRekey>] every rekey declared so far, this one last
157
+ # @raise [Bluebook::DSL::Malformed] if `sql` is empty
104
158
  def rekey_impl(sql:)
105
159
  raise Malformed, "a rekey needs its sql: expression" if sql.to_s.empty?
106
160
 
107
161
  @rekeys << TranslationRekey.new(sql.to_s)
108
162
  end
109
163
 
164
+ # Declares a newly added, required attribute and the default existing records read
165
+ # until a real value is written.
166
+ #
110
167
  # A newly added, required attribute — the addition-side sibling of
111
168
  # `drop`. Nothing to rename, move, or convert from, since old data
112
169
  # never held this field at all; `default` is what an existing
@@ -118,6 +175,12 @@ module Hecks
118
175
  # `EraGuard.refuse_unsafe_addition!` asks for when a non-optional
119
176
  # attribute with no default: could leave an existing record with
120
177
  # the field genuinely absent.
178
+ #
179
+ # @param name [String, Symbol] the new attribute's name
180
+ # @param default [Object] the value an existing record reads until it is written for real
181
+ # @return [Array<Bluebook::TranslationBackfill>] every backfill declared so far, this
182
+ # one last
183
+ # @raise [Bluebook::DSL::Malformed] if `name` is empty or `default` is nil
121
184
  def backfill_impl(name, default:)
122
185
  raise Malformed, "a backfill needs a name" if name.to_s.empty?
123
186
  raise Malformed, "a backfill needs a default: value" if default.nil?
@@ -125,33 +188,46 @@ module Hecks
125
188
  @backfills << TranslationBackfill.new(name.to_sym, default)
126
189
  end
127
190
 
191
+ # Always refuses to boot: marks a field the scaffold could not decide a rule for.
192
+ #
128
193
  # The scaffold writes this where it cannot decide; a file carrying
129
194
  # one can only boot into this refusal — never a guess.
130
195
  #
131
- # Renamed from `unresolved` — item #13's full metaprogrammed
196
+ # Answers the `unresolved` word through the table's `calls:`
197
+ # column — item #13's full metaprogrammed
132
198
  # dispatch (slice 4). Builds its own message with real branching
133
199
  # (empty vs. named candidates, a special :identity case), not a
134
- # fixed string a boolean `refuses:` flag could express — reached
135
- # through `calls:` instead, like `attribute`/`role`. Not
136
- # bootstrap-reachable: translation.bluebook (loaded during
137
- # bootstrap, to describe the translation DSL itself) never
138
- # writes `unresolved` — that word is only ever used by real,
139
- # user-authored `.translation` files, loaded well after the
140
- # grammar table exists — so no BOOTSTRAP_CALLS_FALLBACK entry is
141
- # needed here (checked directly, not assumed).
200
+ # fixed string a boolean `refuses:` flag could express. Carried in
201
+ # `GenericDispatch::BOOTSTRAP_CALLS_FALLBACK` like every other
202
+ # `calls:`-routed word, but that row is never consulted during
203
+ # real bootstrap (checked directly, not assumed):
204
+ # translation.bluebook (loaded during bootstrap, to describe the
205
+ # translation DSL itself) never writes `unresolved` — that word is
206
+ # only ever used by real, user-authored `.translation` files,
207
+ # loaded well after the grammar table already exists.
208
+ #
209
+ # @param name [String, Symbol] the unresolved field's name, or `:identity` for an
210
+ # unresolved identity change
211
+ # @param candidates [Array<String, Symbol>] paths the scaffold considered but could not
212
+ # choose between; empty when it found none
213
+ # @return [void]
214
+ # @raise [Bluebook::DSL::Malformed] always
142
215
  def unresolved_impl(name, candidates: [])
143
216
  raise Malformed, unresolved_message(name, candidates)
144
217
  end
145
218
 
146
- # `method_missing`/`respond_to_missing?` used to be hand-written
147
- # here, giving a hand-typed "must be rename, move, convert, ..."
148
- # list on every genuinely undefined call — WordGate (`include`d
149
- # above) now answers the same question off the self-hosted
150
- # grammar table instead, the exact "hardcoded legal-word list"
151
- # this whole arc's item #13 exists to close. A word admitted
152
- # elsewhere in the grammar but not in this context still gets a
153
- # richer, table-driven refusal than the old generic one did.
219
+ # `method_missing`/`respond_to_missing?` answer off the self-hosted
220
+ # grammar table (via the `include`d `WordGate`, above), giving a
221
+ # richer, table-driven "must be rename, move, convert, ..." refusal
222
+ # on a genuinely undefined call than a hand-typed list could —
223
+ # the exact "hardcoded legal-word list" this whole arc's item #13
224
+ # exists to close. A word admitted
225
+ # elsewhere in the grammar but not in this context still gets that
226
+ # richer refusal.
154
227
 
228
+ # Assembles the declared rules into a `TranslationAggregate`.
229
+ #
230
+ # @return [Bluebook::TranslationAggregate] the built per-aggregate translation
155
231
  def build
156
232
  TranslationAggregate.new(
157
233
  name: @name, was: @was, renames: @renames, moves: @moves, converts: @converts,
@@ -199,6 +275,10 @@ module Hecks
199
275
 
200
276
  include WordGate
201
277
 
278
+ # @param domain [String, Symbol] the domain this translation carries forward
279
+ # @param from [String, Symbol] the origin era
280
+ # @param to [String, Symbol] the destination era
281
+ # @raise [Bluebook::DSL::Malformed] if `domain`, `from`, or `to` is empty
202
282
  def initialize(domain, from:, to:)
203
283
  raise Malformed, "a translation names no domain" if domain.to_s.empty?
204
284
  raise Malformed, "#{domain}'s translation says nothing about its origin era (from:)" if from.to_s.empty?
@@ -211,38 +291,64 @@ module Hecks
211
291
  @retired = []
212
292
  end
213
293
 
214
- # Renamed from `aggregate` — item #13's full metaprogrammed
294
+ # Declares one aggregate's own translation rules.
295
+ #
296
+ # Answers the `aggregate` word through the table's `calls:`
297
+ # column — item #13's full metaprogrammed
215
298
  # dispatch (slice 4c). Not bootstrap-reachable — this "Translation"
216
299
  # -context `aggregate` (opens a TranslationAggregateBuilder) is a
217
300
  # different (context, word) pair than "Bluebook"-context
218
301
  # `aggregate` (the one translation.bluebook itself is described
219
- # with), so it's never used to describe the language's own
302
+ # with), so it never describes the language's own
220
303
  # translation chapter.
304
+ #
305
+ # @param name [String, Symbol] the aggregate's name in the destination era
306
+ # @param was [String, Symbol, nil] the aggregate's earlier name, when renamed
307
+ # @yield the aggregate's own translation body, evaluated against a
308
+ # `TranslationAggregateBuilder`
309
+ # @return [Array<Bluebook::TranslationAggregate>] every aggregate translation declared
310
+ # so far, this one last
311
+ # @raise [Bluebook::DSL::Malformed] if `name` is empty, or any rule in the body fails
312
+ # its own checks
221
313
  def aggregate_impl(name, was: nil, &block)
222
314
  builder = TranslationAggregateBuilder.new(name, was: was)
223
315
  builder.instance_eval(&block) if block
224
316
  @aggregates << builder.build
225
317
  end
226
318
 
227
- # `retired` — item #13's full metaprogrammed dispatch, slice 2
228
- # (whole-project table-unification survey): an aggregate that is
229
- # gone outright — not renamed. The deliberate alternative to a
230
- # bogus `was:` claim on an unrelated aggregate. Same shape
231
- # `TranslationAggregateBuilder#drop` is, now executed by
232
- # `GenericDispatch`.
233
-
234
- # `method_missing`/`respond_to_missing?` — same removal as
235
- # TranslationAggregateBuilder's own, one level up: WordGate
236
- # (`include`d above) answers off the self-hosted grammar table
237
- # now instead of a hand-typed "it declares aggregate blocks and
238
- # retired aggregates" message.
239
-
319
+ # `retired` (an aggregate that is gone outright, not renamed — the
320
+ # deliberate alternative to a bogus `was:` claim on an unrelated
321
+ # aggregate, the same shape `TranslationAggregateBuilder#drop` is)
322
+ # is executed straight off the grammar table by `GenericDispatch` —
323
+ # item #13's full metaprogrammed dispatch, slice 2 (whole-project
324
+ # table-unification survey) — so no hand-written method answers it here.
325
+
326
+ # `method_missing`/`respond_to_missing?` answer off the self-hosted
327
+ # grammar table (via the `include`d `WordGate`, above, the same
328
+ # mechanism `TranslationAggregateBuilder`'s own comment describes
329
+ # one level up) instead of a hand-typed "it declares aggregate
330
+ # blocks and retired aggregates" message.
331
+
332
+ # Assembles the declared era pair, aggregates and retirements, judged by the translation
333
+ # language.
334
+ #
335
+ # @return [Bluebook::Translation] the translation, returned once the language accepts it
336
+ # @raise [Bluebook::DSL::Malformed] if the translation language refuses the declaration
240
337
  def build
241
338
  MetaValidator.call_translation(
242
339
  Translation.new(domain: @domain, from: @from, to: @to, aggregates: @aggregates, retired: @retired)
243
340
  )
244
341
  end
245
342
 
343
+ # Evaluates a `.translation` file's top-level block against a fresh builder.
344
+ #
345
+ # @param domain [String, Symbol] the domain this translation carries forward
346
+ # @param from [String, Symbol] the origin era
347
+ # @param to [String, Symbol] the destination era
348
+ # @yield the translation body, evaluated with the builder as `self`; may be omitted
349
+ # @return [Bluebook::Translation] the judged translation
350
+ # @raise [Bluebook::DSL::Malformed] if the body fails any check, or the translation
351
+ # language refuses the declaration
246
352
  def self.build(domain, from:, to:, &block)
247
353
  builder = new(domain, from: from, to: to)
248
354
  builder.instance_eval(&block) if block