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
@@ -23,6 +23,10 @@ module Hecks
23
23
  # as `--foo`'s value (and `--bar` would then never be seen at
24
24
  # all), and a value-less `--foo` at the end of argv would bypass
25
25
  # whatever default `foo` promised instead of falling back to it.
26
+ # @param argv [Array<String>] the command-line argument list to scan
27
+ # @param name [String] the flag's name, without its leading `--`
28
+ # @param default [String, nil] value to use when the flag is absent or has no value
29
+ # @return [String, nil] the flag's value, or `default`
26
30
  def option(argv, name, default = nil)
27
31
  index = argv.index("--#{name}")
28
32
  return default unless index
@@ -42,6 +46,12 @@ module Hecks
42
46
  # the gate specs, not on anything this method knows about). A
43
47
  # clean return leaves the snapshots unused; the caller decides
44
48
  # from there whether the tree stands.
49
+ #
50
+ # @param paths [Array<String>] files to snapshot before running the block
51
+ # @yield the mutating work to run, restored from snapshot if it raises
52
+ # @return [Object] the block's result
53
+ # @raise [StandardError] re-raises whatever the block raised, after restoring
54
+ # every snapshotted file
45
55
  def restore_on_raise(paths)
46
56
  snapshots = paths.to_h { |path| [path, File.read(path)] }
47
57
  yield
@@ -50,6 +60,10 @@ module Hecks
50
60
  raise
51
61
  end
52
62
 
63
+ # Every syntax-table file declaring a `KeywordSeed` or `ArgumentSeed`
64
+ # value object.
65
+ #
66
+ # @return [Array<String>] matching `.bluebook` file paths
53
67
  def syntax_paths
54
68
  Dir.glob(File.expand_path("../language/**/*.bluebook", __dir__)).select do |path|
55
69
  source = File.read(path)
@@ -60,13 +74,25 @@ module Hecks
60
74
  # Kept as a narrow compatibility door for callers deliberately doing
61
75
  # single-file surgery. Normal operation uses `syntax_paths` and discovers
62
76
  # the owning concept from the row itself.
77
+ #
78
+ # @return [String, nil] the first syntax-table path, or nil when there are none
63
79
  def syntax_path = syntax_paths.first
64
80
 
81
+ # Resolves the file(s) a call should search or write.
82
+ #
83
+ # @param path [String, nil] an explicit single file, or nil for every syntax path
84
+ # @return [Array<String>] `[path]` when given, else `syntax_paths`
65
85
  def paths_for(path) = path ? Array(path) : syntax_paths
66
86
 
67
87
  # The Keyword one_of's member rows, parsed leniently off the text —
68
88
  # enough to know each row's (word, context, status), which is all
69
89
  # the tool ever asks.
90
+ #
91
+ # @param path [String, nil] an explicit single file to read, or nil for every
92
+ # syntax path
93
+ # @return [Array<Hash>] one Hash per member row, with `:word`, `:context`,
94
+ # `:status` (`"admitted"` when unspelled), and `:was` (nil unless renamed)
95
+ # @raise [Refusal] if `path` is given and declares no `KeywordSeed` value object
70
96
  def keyword_rows(path = nil)
71
97
  paths_for(path).flat_map do |candidate|
72
98
  blocks = seed_blocks(File.read(candidate), "KeywordSeed")
@@ -82,6 +108,20 @@ module Hecks
82
108
  end
83
109
  end
84
110
 
111
+ # Declares a new, proposed keyword row in the syntax table that owns
112
+ # `context` (or `opens`'s own aggregate, for a `File`-context word).
113
+ #
114
+ # @param word [String] the keyword's spelling
115
+ # @param context [String] the grammar context the word is declared in
116
+ # @param body [String] the keyword's body shape; `"none"` by default
117
+ # @param inner [String] the keyword's inner shape, if any
118
+ # @param opens [String] the aggregate concept a `File`-context word opens
119
+ # @param fills [String] the field the keyword fills, if any
120
+ # @param path [String, nil] an explicit single file to search/write, or nil to
121
+ # search every syntax path
122
+ # @return [void]
123
+ # @raise [Refusal] if `context`.`word` is already declared, or no syntax table
124
+ # owns `context`
85
125
  def propose(word:, context:, body: "none", inner: "", opens: "", fills: "", path: nil)
86
126
  if keyword_rows(path).any? do |row|
87
127
  row[:word] == word && row[:context] == context
@@ -105,6 +145,17 @@ module Hecks
105
145
  File.write(path, source.sub(block, updated))
106
146
  end
107
147
 
148
+ # Rewrites a declared keyword row's `status:` cell in place.
149
+ #
150
+ # @param word [String] the keyword's spelling
151
+ # @param context [String] the grammar context the word is declared in
152
+ # @param to [String] the new status: `"proposed"`, `"admitted"`, `"deprecated"`,
153
+ # or `"retired"`
154
+ # @param path [String, nil] an explicit single file to search/write, or nil to
155
+ # search every syntax path
156
+ # @return [void]
157
+ # @raise [Refusal] if `to` is not one of the four stations, or `context`.`word`
158
+ # is not declared
108
159
  def set_status(word:, context:, to:, path: nil)
109
160
  raise Refusal, "#{to.inspect} is not a station a word's life admits" unless %w[proposed admitted deprecated
110
161
  retired].include?(to)
@@ -131,8 +182,17 @@ module Hecks
131
182
  # `was:` — one hop only. Renaming an already-renamed word refuses
132
183
  # until the language grows real eras for its own words; renaming
133
184
  # onto a spelling the context already declares refuses too. The
134
- # word's Argument rows follow it — row-aware now, not the blind
135
- # substitution this used to be (see `cascade_argument_rename`).
185
+ # word's Argument rows follow it — row-aware, not a blind
186
+ # substitution (see `cascade_argument_rename`).
187
+ #
188
+ # @param word [String] the keyword's current spelling
189
+ # @param context [String] the grammar context the word is declared in
190
+ # @param to [String] the keyword's new spelling
191
+ # @param path [String, nil] an explicit single file to search/write, or nil to
192
+ # search every syntax path
193
+ # @return [void]
194
+ # @raise [Refusal] if `context`.`word` is not declared, was already renamed once,
195
+ # or `to` is already declared in `context`
136
196
  def rename(word:, context:, to:, path: nil)
137
197
  row = keyword_rows(path).find { |r| r[:word] == word && r[:context] == context }
138
198
  raise Refusal, "#{context}.#{word} is not declared" unless row
@@ -160,16 +220,38 @@ module Hecks
160
220
  cascade_argument_rename(keyword: word, context: context, to: to, path: paths)
161
221
  end
162
222
 
223
+ # Tells whether `line` is a KeywordSeed member row for `(word, context)`.
224
+ #
225
+ # @param line [String] one raw source line
226
+ # @param word [String] the keyword's spelling
227
+ # @param context [String] the grammar context
228
+ # @return [Boolean]
163
229
  def member_row?(line, word, context)
164
230
  line =~ /^\s*member / && line.include?(%(word: "#{word}")) && line.include?(%(context: "#{context}"))
165
231
  end
166
232
 
233
+ # Finds which syntax-table path declares a keyword row.
234
+ #
235
+ # @param word [String] the keyword's spelling
236
+ # @param context [String] the grammar context
237
+ # @param paths [Array<String>] candidate syntax-table paths to search
238
+ # @return [String] the path whose KeywordSeed declares `(word, context)`
239
+ # @raise [Refusal] if no path in `paths` declares that row
167
240
  def path_holding_keyword(word, context, paths = syntax_paths)
168
241
  paths.find do |candidate|
169
242
  keyword_blocks(File.read(candidate)).any? { |block| block.lines.any? { |line| member_row?(line, word, context) } }
170
243
  end || raise(Refusal, "#{context}.#{word} is not declared")
171
244
  end
172
245
 
246
+ # Finds which syntax-table path declares an argument row.
247
+ #
248
+ # @param keyword [String] the argument's owning keyword
249
+ # @param context [String] the grammar context
250
+ # @param at [String] the argument's positional slot, `""` for a named-only argument
251
+ # @param named [String] the argument's keyword name, `""` for a positional-only argument
252
+ # @param paths [Array<String>] candidate syntax-table paths to search
253
+ # @return [String] the path whose ArgumentSeed declares this row
254
+ # @raise [Refusal] if no path in `paths` declares that row
173
255
  def path_holding_argument(keyword, context, at, named, paths = syntax_paths)
174
256
  paths.find do |candidate|
175
257
  argument_blocks(File.read(candidate)).any? do |block|
@@ -183,6 +265,14 @@ module Hecks
183
265
  # A new row belongs wherever that context's existing rows live. File is
184
266
  # intentionally wider than one aggregate; for a new entry point, `opens`
185
267
  # identifies the aggregate concept whose file should own it.
268
+ #
269
+ # @param context [String] the grammar context a new row is being added to
270
+ # @param word [String] the word being added, used only in the refusal message
271
+ # @param opens [String] the aggregate concept a `File`-context word opens
272
+ # @param paths [Array<String>] candidate syntax-table paths to search
273
+ # @return [String] the path that should own the new row
274
+ # @raise [Refusal] if no candidate path declares an existing row for `context`
275
+ # (and, for a `File` context with `opens` given, no path declares that aggregate)
186
276
  def owner_path(context:, word:, opens: "", paths: syntax_paths)
187
277
  if context == "File" && !opens.to_s.empty?
188
278
  aggregate_path = paths.find { |candidate| File.read(candidate).match?(/^\s*aggregate "#{Regexp.escape(opens)}" do$/) }
@@ -197,6 +287,13 @@ module Hecks
197
287
  end || raise(Refusal, "no aggregate-local syntax table owns context #{context.inspect} for #{word}")
198
288
  end
199
289
 
290
+ # Every `value_object "<name>"` block's full source text, from its
291
+ # opener to its closing `end`.
292
+ #
293
+ # @param source [String] a `.bluebook` file's source text
294
+ # @param name [String] the value object's name, such as `"KeywordSeed"`
295
+ # @return [Array<String>] each matching block's raw source, including the
296
+ # opener and closing `end` lines
200
297
  def seed_blocks(source, name)
201
298
  opener = /^([ \t]*)value_object "#{Regexp.escape(name)}" do$/
202
299
  source.to_enum(:scan, opener).map do
@@ -209,6 +306,15 @@ module Hecks
209
306
  end
210
307
  end
211
308
 
309
+ # The first `value_object "<name>"` block's source text.
310
+ #
311
+ # @param source [String] a `.bluebook` file's source text
312
+ # @param name [String] the value object's name, such as `"KeywordSeed"`
313
+ # @param required [Boolean] whether a missing block should raise instead of
314
+ # returning nil
315
+ # @return [String, nil] the block's raw source, or nil when absent and not
316
+ # `required`
317
+ # @raise [Refusal] if `required` and `source` declares no such value object
212
318
  def seed_block(source, name, required: false)
213
319
  block = seed_blocks(source, name).first
214
320
  unless block
@@ -225,7 +331,15 @@ module Hecks
225
331
  # is the value object's own, the same fact the original one_of-
226
332
  # nested version of this method leaned on (nothing else nested
227
333
  # inside it either, before or after).
334
+ # @param source [String] a `.bluebook` file's source text
335
+ # @return [Array<String>] each KeywordSeed block's raw source
228
336
  def keyword_blocks(source) = seed_blocks(source, "KeywordSeed")
337
+
338
+ # The first KeywordSeed block's source text.
339
+ #
340
+ # @param source [String] a `.bluebook` file's source text
341
+ # @return [String] the first KeywordSeed block's raw source
342
+ # @raise [Refusal] if `source` declares no KeywordSeed value object
229
343
  def keyword_block(source) = seed_block(source, "KeywordSeed", required: true)
230
344
 
231
345
  # ── the Argument rows — a word's own arguments, at last with tooling
@@ -234,6 +348,14 @@ module Hecks
234
348
  # kwarg), so identity here is the full (keyword, context, at, named)
235
349
  # tuple, not the two-field key a Keyword row answers to.
236
350
 
351
+ # The ArgumentSeed's member rows, parsed leniently off the text.
352
+ #
353
+ # @param path [String, nil] an explicit single file to read, or nil for every
354
+ # syntax path
355
+ # @return [Array<Hash>] one Hash per member row, with `:keyword`, `:context`,
356
+ # `:at`, `:named`, `:kind`, `:required`, `:fills`, and `:status`
357
+ # (`"admitted"` when unspelled)
358
+ # @raise [Refusal] if `path` is given and declares no `ArgumentSeed` value object
237
359
  def argument_rows(path = nil)
238
360
  paths_for(path).flat_map do |candidate|
239
361
  blocks = seed_blocks(File.read(candidate), "ArgumentSeed")
@@ -257,6 +379,20 @@ module Hecks
257
379
  # naming a single field as a row that "names a single field, which
258
380
  # it cannot fill" — correctly, since the two shapes are genuinely
259
381
  # different and only one of them can be checked the same way.
382
+ # @param keyword [String] the argument's owning keyword
383
+ # @param context [String] the grammar context
384
+ # @param kind [String] the argument's value kind
385
+ # @param required [String] `"true"` or `"false"`, as text like every other cell
386
+ # @param at [String] the argument's positional slot, `""` for a named-only argument
387
+ # @param named [String] the argument's keyword name, `""` for a positional-only argument
388
+ # @param fills [String] the field the argument fills, if any
389
+ # @param pairs_shape [String, nil] the shape a `pairs` argument's key/value list
390
+ # fills, or nil when this argument is not a `pairs` argument
391
+ # @param path [String, nil] an explicit single file to search/write, or nil to
392
+ # search every syntax path
393
+ # @return [void]
394
+ # @raise [Refusal] if this (keyword, context, at, named) row is already declared,
395
+ # or no syntax table owns `context`
260
396
  def propose_argument(keyword:, context:, kind:, required: "false", at: "", named: "", fills: "",
261
397
  pairs_shape: nil, path: nil)
262
398
  if argument_rows(path).any? { |r| argument_identity(r) == [keyword, context, at, named] }
@@ -280,6 +416,18 @@ module Hecks
280
416
  File.write(path, source.sub(block, updated))
281
417
  end
282
418
 
419
+ # Rewrites a declared argument row's `status:` cell in place.
420
+ #
421
+ # @param keyword [String] the argument's owning keyword
422
+ # @param context [String] the grammar context
423
+ # @param to [String] the new status: `"proposed"`, `"admitted"`, `"deprecated"`,
424
+ # or `"retired"`
425
+ # @param at [String] the argument's positional slot, `""` for a named-only argument
426
+ # @param named [String] the argument's keyword name, `""` for a positional-only argument
427
+ # @param path [String, nil] an explicit single file to search/write, or nil to
428
+ # search every syntax path
429
+ # @return [void]
430
+ # @raise [Refusal] if `to` is not one of the four stations, or the row is not declared
283
431
  def set_argument_status(keyword:, context:, to:, at: "", named: "", path: nil)
284
432
  raise Refusal, "#{to.inspect} is not a station an argument's life admits" unless %w[proposed admitted deprecated
285
433
  retired].include?(to)
@@ -307,12 +455,26 @@ module Hecks
307
455
  File.write(path, source.sub(block, updated))
308
456
  end
309
457
 
458
+ # Tells whether `line` is an ArgumentSeed member row for this
459
+ # (keyword, context, at, named) tuple.
460
+ #
461
+ # @param line [String] one raw source line
462
+ # @param keyword [String] the argument's owning keyword
463
+ # @param context [String] the grammar context
464
+ # @param at [String] the argument's positional slot
465
+ # @param named [String] the argument's keyword name
466
+ # @return [Boolean]
310
467
  def argument_row?(line, keyword, context, at, named)
311
468
  line =~ /^\s*member / &&
312
469
  line.include?(%(keyword: "#{keyword}")) && line.include?(%(context: "#{context}")) &&
313
470
  line.include?(%(at: "#{at}")) && line.include?(%(named: "#{named}"))
314
471
  end
315
472
 
473
+ # The identity tuple an argument row is keyed by.
474
+ #
475
+ # @param row [Hash] an argument row, as `argument_rows` returns one
476
+ # @return [Array(String, String, String, String)] the row's `(keyword, context,
477
+ # at, named)` identity tuple
316
478
  def argument_identity(row) = [row[:keyword], row[:context], row[:at], row[:named]]
317
479
 
318
480
  # The rename cascade, row-aware — only the rows that actually belong
@@ -320,6 +482,12 @@ module Hecks
320
482
  # blind `gsub` on every `keyword: "word",` substring in the file
321
483
  # (which a coincidentally-matching row elsewhere could have
322
484
  # corrupted, and which read nothing before writing).
485
+ # @param keyword [String] the keyword whose argument rows follow its rename
486
+ # @param context [String] the grammar context
487
+ # @param to [String] the keyword's new spelling
488
+ # @param path [String, nil] an explicit single file to search/write, or nil to
489
+ # search every syntax path
490
+ # @return [void]
323
491
  def cascade_argument_rename(keyword:, context:, to:, path: nil)
324
492
  paths_for(path).each do |candidate|
325
493
  source = File.read(candidate)
@@ -341,7 +509,15 @@ module Hecks
341
509
  # `keyword_block`'s own comment for why the first bare `end` after
342
510
  # the opener is already the right one, now that `member` rows sit
343
511
  # bare (S3, ADR 0025).
512
+ # @param source [String] a `.bluebook` file's source text
513
+ # @return [Array<String>] each ArgumentSeed block's raw source
344
514
  def argument_blocks(source) = seed_blocks(source, "ArgumentSeed")
515
+
516
+ # The first ArgumentSeed block's source text.
517
+ #
518
+ # @param source [String] a `.bluebook` file's source text
519
+ # @return [String] the first ArgumentSeed block's raw source
520
+ # @raise [Refusal] if `source` declares no ArgumentSeed value object
345
521
  def argument_block(source) = seed_block(source, "ArgumentSeed", required: true)
346
522
  end
347
523
  end
data/lib/hecks/grammar.rb CHANGED
@@ -27,6 +27,11 @@ module Hecks
27
27
  # its real commands. A refused step raises — a generator running off
28
28
  # a half-admitted ledger would project a table the gates never
29
29
  # accepted.
30
+ #
31
+ # @return [Runtime::Dispatcher] the dispatcher bound to the booted, replayed
32
+ # expression chapter
33
+ # @raise [Runtime::WiringError] if a ledger step is refused by the chapter's
34
+ # own domain rules
30
35
  def expression
31
36
  registry = Runtime::Registry.new
32
37
  root = File.expand_path("../..", __dir__)
@@ -52,6 +57,13 @@ module Hecks
52
57
  dispatcher
53
58
  end
54
59
 
60
+ # Reads every admitted operator from the ledger's replayed chapter.
61
+ #
62
+ # @param dispatcher [Runtime::Dispatcher] a dispatcher bound to the booted
63
+ # expression chapter; defaults to booting and replaying a fresh one
64
+ # @return [Array<Hash>] one Hash per admitted operator, with `:symbol`,
65
+ # `:category`, `:precedence`, `:arity`, and `:renderings` (an Array of
66
+ # `{target:, form:}` Hashes)
55
67
  def admitted_operators(dispatcher = expression)
56
68
  records(dispatcher, "Operator").select { |op| op[:status] == "admitted" }.map do |op|
57
69
  { symbol: op[:symbol].value, category: op[:category].value,
@@ -60,6 +72,13 @@ module Hecks
60
72
  end
61
73
  end
62
74
 
75
+ # Reads every admitted normalisation rule from the ledger's replayed
76
+ # chapter, in position order.
77
+ #
78
+ # @param dispatcher [Runtime::Dispatcher] a dispatcher bound to the booted
79
+ # expression chapter; defaults to booting and replaying a fresh one
80
+ # @return [Array<Hash>] one Hash per admitted rule, with `:strategy`,
81
+ # `:source_token`, `:replacement`, `:boundary`, and `:position`
63
82
  def admitted_normalisations(dispatcher = expression)
64
83
  records(dispatcher, "Normalisation")
65
84
  .select { |rule| rule[:status] == "admitted" }
@@ -71,6 +90,14 @@ module Hecks
71
90
  end
72
91
  end
73
92
 
93
+ # Reads every record of one aggregate from the expression chapter's own
94
+ # repository.
95
+ #
96
+ # @param dispatcher [Runtime::Dispatcher] a dispatcher bound to the booted
97
+ # expression chapter
98
+ # @param aggregate_name [String] the aggregate's declared name, such as
99
+ # `"Operator"`
100
+ # @return [Array<Runtime::Instance>] every stored instance of the aggregate
74
101
  def records(dispatcher, aggregate_name)
75
102
  registry = dispatcher.registry
76
103
  aggregate = registry.bluebook("Expression").aggregate(aggregate_name)
@@ -87,6 +114,10 @@ module Hecks
87
114
  # derives the set, with a usage site per operator, so the generator
88
115
  # and the conformance spec can refuse the retirement by name instead
89
116
  # of wedging.
117
+ #
118
+ # @return [Hash{String => Array<String>}] each self-bearing operator symbol
119
+ # mapped to the `"Chapter Aggregate.command"`/`"Chapter Aggregate::ValueObject"`
120
+ # sites that use it
90
121
  def self_bearing_operators
91
122
  sites = Hash.new { |h, k| h[k] = [] }
92
123
 
@@ -118,6 +149,9 @@ module Hecks
118
149
 
119
150
  # Every grammar/*.bluebook chapter, booted the same way the corpus
120
151
  # boots them — each alone, in a scratch registry.
152
+ #
153
+ # @return [Array<Class>] each grammar chapter, booted alone in its own scratch
154
+ # registry
121
155
  def grammar_chapters
122
156
  Dir[File.join(DIR, "*.bluebook")].map do |chapter|
123
157
  registry = Runtime::Registry.new
@@ -136,6 +170,10 @@ module Hecks
136
170
  # Which admitted operators one canonical text evaluates through —
137
171
  # the evaluator's own parse, walked for its operator nodes, leaves
138
172
  # walked for the resolver's arithmetic.
173
+ #
174
+ # @param canonical [String] canonical expression text to parse
175
+ # @return [Array<String>] operator symbols the expression evaluates through, or
176
+ # `[]` when `canonical` fails to parse
139
177
  def operators_in(canonical)
140
178
  evaluator = Bluebook::Expression::Evaluator
141
179
  begin
@@ -155,6 +193,10 @@ module Hecks
155
193
  # would trade one place that shows the whole operator vocabulary for
156
194
  # several that each show a fragment, with no reduction in real
157
195
  # complexity.
196
+ # @param node [Object] an evaluator/resolver AST node, or a Struct fallback
197
+ # @param evaluator [Module] `Bluebook::Expression::Evaluator`, passed through
198
+ # so nested calls don't re-resolve the constant
199
+ # @return [Array<String>] operator symbols found in `node` and its children
158
200
  # rubocop:disable-next Metrics/AbcSize
159
201
  def walk_operators(node, evaluator)
160
202
  resolver = Bluebook::Expression::Resolver
@@ -177,6 +219,10 @@ module Hecks
177
219
  end
178
220
  end
179
221
 
222
+ # Deep-symbolizes a JSON-decoded value's Hash keys.
223
+ #
224
+ # @param value [Object] a Hash, Array, or scalar decoded from JSON
225
+ # @return [Object] `value` with every Hash key (recursively) converted to a Symbol
180
226
  def symbolize(value)
181
227
  case value
182
228
  when Hash then value.to_h { |k, v| [k.to_sym, symbolize(v)] }
data/lib/hecks/ir.rb CHANGED
@@ -17,13 +17,16 @@ module Hecks
17
17
  # nesting everything a chapter declares. It is deliberately not named
18
18
  # after this, its own output.
19
19
  #
20
- # Before this, eighteen constructs each hand-wrote a `to_h` that said
21
- # the same four things in the same order — read a field, recurse into a
22
- # child, recurse into a list, or compute something — and the shape of a
23
- # construct was knowable only by reading a method body. Declared, it is
24
- # data: `ir_spec` can be walked by anything that wants to know what a
25
- # construct carries, which is the whole point of hanging emission off
26
- # the model rather than burying it.
20
+ # ## Why declared, not hand-written
21
+ #
22
+ # A hand-written `to_h` per construct would say the same four things in the
23
+ # same order — read a field, recurse into a child, recurse into a list, or
24
+ # compute something — leaving a construct's shape knowable only by reading a
25
+ # method body. Declared, it is data: `ir_spec` can be walked by anything
26
+ # that wants to know what a construct carries, which is the whole point of
27
+ # hanging emission off the model rather than burying it.
28
+ #
29
+ # ## Usage
27
30
  #
28
31
  # include Hecks::IR # an instance-shaped construct
29
32
  #
@@ -57,11 +60,20 @@ module Hecks
57
60
  # extend Hecks::IR # class-shaped — to_h is a class method
58
61
  #
59
62
  # Both get the same `emits_ir` and the same emission rules.
63
+ #
64
+ # Wires an instance-shaped construct's declaration and emission sides in.
65
+ #
66
+ # @param base [Class, Module] the includer
67
+ # @return [void]
60
68
  def self.included(base)
61
69
  base.extend(Declares)
62
70
  base.include(Emits)
63
71
  end
64
72
 
73
+ # Wires a class-shaped construct's declaration and emission sides in.
74
+ #
75
+ # @param base [Class, Module] the extender
76
+ # @return [void]
65
77
  def self.extended(base)
66
78
  base.extend(Declares)
67
79
  base.extend(Emits)
@@ -79,17 +91,36 @@ module Hecks
79
91
  # back — walking the superclass chain so an anonymous `Class.new(base)`
80
92
  # inherits its base's shape instead of redeclaring it.
81
93
  module Declares
94
+ # Records this construct's field -> emission-rule map.
95
+ #
96
+ # @param spec [Hash{Symbol => Symbol, Many, One, Proc}] each emitted key,
97
+ # mapped to how it is produced: a Symbol is sent to the construct, `many`/
98
+ # `one` recurse into nested constructs, and a Proc is instance-`exec`'d
99
+ # @return [void]
82
100
  def emits_ir(**spec)
83
101
  @ir_spec = spec
84
102
  end
85
103
 
104
+ # Marks a field as a list of nested constructs, each emitting itself.
105
+ #
106
+ # @param source [Symbol] the method that returns the list
107
+ # @return [Many] the wrapped source, for `emits_ir`
86
108
  def many(source) = Many.new(source)
109
+
110
+ # Marks a field as one nested construct, or nothing.
111
+ #
112
+ # @param source [Symbol] the method that returns the construct, or nil
113
+ # @return [One] the wrapped source, for `emits_ir`
87
114
  def one(source) = One.new(source)
88
115
 
89
116
  # Walks the superclass chain so a `Class.new(ValueObject)` — which
90
117
  # is what every declared value object actually is — inherits the
91
118
  # shape its base declared, rather than each anonymous subclass
92
119
  # having to redeclare it.
120
+ #
121
+ # @return [Hash{Symbol => Symbol, Many, One, Proc}, nil] the field -> rule
122
+ # map declared by `emits_ir`, inherited from the nearest superclass that
123
+ # declared one; nil if nothing in the chain ever declared a shape
93
124
  def ir_spec
94
125
  return @ir_spec if defined?(@ir_spec) && @ir_spec
95
126
 
@@ -100,6 +100,16 @@ Hecks.bluebook "Hecksagon" do
100
100
  member word: "uses_framework", context: "Hecksagon", body: "none", inner: "", opens: "", fills: "framework_members"
101
101
  member word: "uses_embryonaut_bluebook", context: "Hecksagon", body: "none", inner: "", opens: "", fills: "vendored_bluebooks"
102
102
  member word: "port", context: "Hecksagon", body: "keywords", inner: "DomainPort", opens: "DomainPort", fills: "", calls: "port_impl"
103
+ # A CROSS-DOMAIN REACTION, WIRED HERE INSTEAD OF THE BLUEBOOK — same
104
+ # underlying `Policy` shape a `policy` block already builds
105
+ # (`inner`/`opens: "Policy"` reuses that context's own `on`/`trigger`
106
+ # wholesale, zero new inner vocabulary), but declared as a wiring
107
+ # decision (which FOREIGN domain's event this chapter conforms to),
108
+ # the same kind of decision `port`/`uses_framework` already are.
109
+ # No `calls:` — `HecksagonBuilder#translates` is an ordinary method,
110
+ # the same shape `subscribe`/`uses_framework` already are, reached
111
+ # by Ruby's own method lookup before `word_gate` ever runs.
112
+ member word: "translates", context: "Hecksagon", body: "keywords", inner: "Policy", opens: "Policy", fills: ""
103
113
 
104
114
  end
105
115
 
@@ -125,6 +135,7 @@ Hecks.bluebook "Hecksagon" do
125
135
  member keyword: "uses_framework", context: "Hecksagon", at: "1", named: "", kind: "text", required: "true", fills: "framework_members"
126
136
  member keyword: "uses_embryonaut_bluebook", context: "Hecksagon", at: "1", named: "", kind: "text", required: "true", fills: "vendored_bluebooks"
127
137
  member keyword: "port", context: "Hecksagon", at: "1", named: "", kind: "text", required: "true", fills: "name"
138
+ member keyword: "translates", context: "Hecksagon", at: "1", named: "", kind: "text", required: "true", fills: "name"
128
139
 
129
140
  end
130
141
  end
data/lib/hecks/literal.rb CHANGED
@@ -43,6 +43,12 @@ module Hecks
43
43
  module Literal
44
44
  module_function
45
45
 
46
+ # Turns a Ruby value into its self-describing wire spelling.
47
+ #
48
+ # @param value [Object] value to render: nil, Symbol, String, StateRef, true,
49
+ # false, Integer, Float, Hash, or Array (recursively)
50
+ # @return [String] the self-describing spelling `read` can parse back
51
+ # @raise [ArgumentError] if `value` is a type with no pinned literal spelling
46
52
  def render(value)
47
53
  case value
48
54
  when nil then "nil"
@@ -67,6 +73,9 @@ module Hecks
67
73
  # `return`; splitting them into named predicates would just rename
68
74
  # each line without changing what it does, and would separate this
69
75
  # method from the `render` it is the deliberate mirror of.
76
+ # @param text [String, #to_s] wire spelling produced by `render`, or a bare word
77
+ # @return [Object] nil, true, false, Integer, Float, Symbol, StateRef, String,
78
+ # Hash, or Array — or `text` itself, stripped, when it matches no known spelling
70
79
  # rubocop:disable-next Metrics/CyclomaticComplexity
71
80
  # rubocop:disable-next Metrics/PerceivedComplexity
72
81
  def read(text)
@@ -87,12 +96,28 @@ module Hecks
87
96
 
88
97
  ESCAPED = { '"' => '\\"', "\\" => "\\\\" }.freeze
89
98
 
99
+ # Wraps `text` in double quotes, escaping embedded quotes and backslashes.
100
+ #
101
+ # @param text [String] raw text to quote
102
+ # @return [String] the quoted, escaped spelling
90
103
  def quote(text) = "\"#{text.gsub(/["\\]/) { |char| ESCAPED[char] }}\""
91
104
 
105
+ # Tells whether `raw` is a double-quoted literal.
106
+ #
107
+ # @param raw [String] wire text to check
108
+ # @return [Boolean]
92
109
  def quoted?(raw) = raw.length >= 2 && raw.start_with?('"') && raw.end_with?('"')
93
110
 
111
+ # Strips the surrounding quotes from a quoted literal and unescapes it.
112
+ #
113
+ # @param raw [String] a quoted literal, as `quoted?` would confirm
114
+ # @return [String] the unescaped text inside the quotes
94
115
  def unquote(raw) = raw[1..-2].gsub(/\\(.)/) { ::Regexp.last_match(1) }
95
116
 
117
+ # Parses a `{key: value, ...}` wire literal.
118
+ #
119
+ # @param raw [String] text starting with `{` and ending with `}`
120
+ # @return [Hash{Symbol => Object}] the parsed hash, values read recursively via `read`
96
121
  def read_hash(raw)
97
122
  split_items(raw[1..-2]).to_h do |item|
98
123
  key, _, held = item.partition(":")
@@ -100,12 +125,19 @@ module Hecks
100
125
  end
101
126
  end
102
127
 
128
+ # Parses a `[value, ...]` wire literal.
129
+ #
130
+ # @param raw [String] text starting with `[` and ending with `]`
131
+ # @return [Array<Object>] the parsed values, each read recursively via `read`
103
132
  def read_array(raw) = split_items(raw[1..-2]).map { |item| read(item) }
104
133
 
105
134
  # Split on the commas that are actually separators — never one inside a
106
135
  # quoted string or a nested brace/bracket. Scanned rather than
107
136
  # `String#split(", ")`, which tore `"a, b"` in half and lost the second
108
137
  # field of anything nested.
138
+ #
139
+ # @param body [String] the text between a literal's outer braces or brackets
140
+ # @return [Array<String>] each item's raw text, stripped, with empty items dropped
109
141
  def split_items(body)
110
142
  items = []
111
143
  current = +""