hecks 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) 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.hecksagon +19 -0
  112. data/lib/hecks/doc/reference.rb +185 -16
  113. data/lib/hecks/embryonaut_bluebook.rb +32 -9
  114. data/lib/hecks/facade/handle.rb +76 -3
  115. data/lib/hecks/facade/surface/aggregate_door.rb +8 -0
  116. data/lib/hecks/forms/field_shape.rb +3 -0
  117. data/lib/hecks/forms/page.rb +14 -0
  118. data/lib/hecks/forms/port_argument.rb +12 -0
  119. data/lib/hecks/forms/query_form_renderer.rb +63 -0
  120. data/lib/hecks/forms/record_renderer.rb +58 -0
  121. data/lib/hecks/forms/record_table.rb +27 -0
  122. data/lib/hecks/forms/reference_options.rb +24 -0
  123. data/lib/hecks/forms/value_object_shape.rb +10 -0
  124. data/lib/hecks/fqn.rb +58 -0
  125. data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
  126. data/lib/hecks/framework/oidc.json +15 -0
  127. data/lib/hecks/framework.rb +43 -20
  128. data/lib/hecks/freezer.rb +17 -1
  129. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +159 -24
  130. data/lib/hecks/fuzzing/combination_miner.rb +59 -0
  131. data/lib/hecks/fuzzing/concurrent_dispatch.rb +109 -8
  132. data/lib/hecks/fuzzing/coverage_campaign.rb +56 -13
  133. data/lib/hecks/fuzzing/differential.rb +34 -0
  134. data/lib/hecks/fuzzing/domain_generator.rb +188 -11
  135. data/lib/hecks/fuzzing/era_boundary.rb +45 -15
  136. data/lib/hecks/fuzzing/form_census.rb +86 -0
  137. data/lib/hecks/fuzzing/generated_domain_check.rb +76 -0
  138. data/lib/hecks/fuzzing/invalid_value_generator.rb +39 -0
  139. data/lib/hecks/fuzzing/isolated_boot.rb +79 -22
  140. data/lib/hecks/fuzzing/nondeterministic.rb +13 -1
  141. data/lib/hecks/fuzzing/persistence_parity.rb +95 -3
  142. data/lib/hecks/fuzzing/properties/corrections.rb +25 -0
  143. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +158 -14
  144. data/lib/hecks/fuzzing/properties/guards.rb +44 -0
  145. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +48 -0
  146. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +18 -0
  147. data/lib/hecks/fuzzing/properties/outbox.rb +49 -11
  148. data/lib/hecks/fuzzing/properties/querying.rb +68 -14
  149. data/lib/hecks/fuzzing/properties.rb +24 -15
  150. data/lib/hecks/fuzzing/qa_settings.rb +12 -0
  151. data/lib/hecks/fuzzing/replay.rb +137 -29
  152. data/lib/hecks/fuzzing/rotation_priority.rb +41 -21
  153. data/lib/hecks/fuzzing/rust_gap_manifest.rb +46 -20
  154. data/lib/hecks/fuzzing/self_consistency.rb +189 -40
  155. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +12 -6
  156. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +1 -1
  157. data/lib/hecks/fuzzing/sequence_generator.rb +47 -0
  158. data/lib/hecks/fuzzing/shrinker.rb +114 -2
  159. data/lib/hecks/fuzzing/structural_skips.rb +18 -4
  160. data/lib/hecks/fuzzing/sweep_depth.rb +8 -0
  161. data/lib/hecks/fuzzing/target_capabilities.rb +61 -14
  162. data/lib/hecks/fuzzing/value_generator.rb +98 -10
  163. data/lib/hecks/grammar/evolve.rb +178 -2
  164. data/lib/hecks/grammar.rb +46 -0
  165. data/lib/hecks/ir.rb +38 -7
  166. data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
  167. data/lib/hecks/literal.rb +32 -0
  168. data/lib/hecks/naming.rb +88 -7
  169. data/lib/hecks/ports/access_control.rb +5 -10
  170. data/lib/hecks/ports/authorization.rb +3 -6
  171. data/lib/hecks/ports/identity_assignment.rb +1 -2
  172. data/lib/hecks/ports/identity_resolution.rb +1 -2
  173. data/lib/hecks/ports/key_vault.port +6 -0
  174. data/lib/hecks/ports/key_vault.rb +58 -0
  175. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +6 -0
  176. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +38 -2
  177. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -0
  178. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +40 -0
  179. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +92 -1
  180. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
  181. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +8 -0
  182. data/lib/hecks/ports/query/in_memory.rb +39 -6
  183. data/lib/hecks/ports/query/ordering.rb +15 -0
  184. data/lib/hecks/ports.rb +1 -0
  185. data/lib/hecks/projections/bootstrap_table.rb +43 -8
  186. data/lib/hecks/projections/diagrams.rb +243 -7
  187. data/lib/hecks/projections/glossary/html.rb +88 -0
  188. data/lib/hecks/projections/glossary/markdown.rb +47 -0
  189. data/lib/hecks/projections/glossary/mermaid.rb +48 -0
  190. data/lib/hecks/projections/glossary/sentences.rb +105 -0
  191. data/lib/hecks/projections/glossary.rb +161 -19
  192. data/lib/hecks/projections/model/deviations.rb +44 -0
  193. data/lib/hecks/projections/model.rb +51 -1
  194. data/lib/hecks/projections/oidc.rb +40 -3
  195. data/lib/hecks/projections/parser_table.rb +49 -8
  196. data/lib/hecks/projections/reference.rb +12 -0
  197. data/lib/hecks/projections/rust_vocabulary.rb +219 -16
  198. data/lib/hecks/projections/shape.rb +8 -0
  199. data/lib/hecks/projections/statements.rb +63 -16
  200. data/lib/hecks/projections/vocabulary.rb +17 -0
  201. data/lib/hecks/projector/cli_projector.rb +218 -10
  202. data/lib/hecks/projector/docs_projector.rb +145 -19
  203. data/lib/hecks/projector/exporter.rb +65 -11
  204. data/lib/hecks/projector/ir_projector.rb +6 -0
  205. data/lib/hecks/projector/narrate_projector.rb +136 -15
  206. data/lib/hecks/projector/target.rb +29 -5
  207. data/lib/hecks/projector.rb +74 -6
  208. data/lib/hecks/query_ir.rb +47 -0
  209. data/lib/hecks/query_specification/common/null_policy.rb +5 -3
  210. data/lib/hecks/rendering.rb +6 -0
  211. data/lib/hecks/router/namespace_installer.rb +13 -0
  212. data/lib/hecks/router.rb +55 -0
  213. data/lib/hecks/runtime/aggregate_lock.rb +9 -0
  214. data/lib/hecks/runtime/boot_gates.rb +18 -0
  215. data/lib/hecks/runtime/caller.rb +32 -0
  216. data/lib/hecks/runtime/capability_graph.rb +11 -0
  217. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +23 -21
  218. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +14 -15
  219. data/lib/hecks/runtime/command_interpreter.rb +42 -17
  220. data/lib/hecks/runtime/command_rules/admissibility.rb +165 -14
  221. data/lib/hecks/runtime/command_rules/arithmetic.rb +17 -5
  222. data/lib/hecks/runtime/command_rules/references.rb +118 -28
  223. data/lib/hecks/runtime/dependency_planning.rb +45 -0
  224. data/lib/hecks/runtime/dispatcher.rb +28 -50
  225. data/lib/hecks/runtime/entity_element.rb +161 -8
  226. data/lib/hecks/runtime/entity_interpreter.rb +44 -9
  227. data/lib/hecks/runtime/errors.rb +18 -4
  228. data/lib/hecks/runtime/event.rb +10 -5
  229. data/lib/hecks/runtime/identity.rb +71 -3
  230. data/lib/hecks/runtime/instance.rb +67 -7
  231. data/lib/hecks/runtime/interpreting.rb +13 -5
  232. data/lib/hecks/runtime/invocation.rb +118 -36
  233. data/lib/hecks/runtime/loader.rb +94 -8
  234. data/lib/hecks/runtime/outbox.rb +145 -7
  235. data/lib/hecks/runtime/policy_interpreter.rb +22 -9
  236. data/lib/hecks/runtime/port_operation_interpreter.rb +20 -0
  237. data/lib/hecks/runtime/query_interpreter.rb +40 -12
  238. data/lib/hecks/runtime/reaction_invocation.rb +53 -8
  239. data/lib/hecks/runtime/read_model_interpreter.rb +23 -7
  240. data/lib/hecks/runtime/rebuild_sweep.rb +28 -0
  241. data/lib/hecks/runtime/reference_hop.rb +42 -0
  242. data/lib/hecks/runtime/refusal_wording.rb +50 -0
  243. data/lib/hecks/runtime/registry/saga_persistence.rb +11 -0
  244. data/lib/hecks/runtime/registry/verification.rb +119 -4
  245. data/lib/hecks/runtime/registry.rb +157 -4
  246. data/lib/hecks/runtime/remote_dispatcher.rb +92 -6
  247. data/lib/hecks/runtime/routing.rb +27 -2
  248. data/lib/hecks/runtime/saga_interpreter/correlation.rb +10 -12
  249. data/lib/hecks/runtime/saga_interpreter.rb +27 -13
  250. data/lib/hecks/runtime/tenant_check.rb +26 -6
  251. data/lib/hecks/runtime/tenant_scope.rb +18 -0
  252. data/lib/hecks/runtime/value/coercion.rb +255 -33
  253. data/lib/hecks/runtime/value/entity_list_coercion.rb +102 -30
  254. data/lib/hecks/runtime/value.rb +50 -0
  255. data/lib/hecks/runtime.rb +32 -0
  256. data/lib/hecks/storehouse.rb +305 -9
  257. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
  258. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
  259. data/lib/hecks/version.rb +1 -1
  260. data/lib/hecks.rb +79 -1
  261. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -0
  262. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +12 -2
  263. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +29 -5
  264. metadata +11 -5
  265. data/lib/hecks/codemod/legacy_dispatch_args.rb +0 -299
  266. data/lib/hecks/codemod/legacy_dispatch_recorder.rb +0 -186
  267. data/lib/hecks/deprecation.rb +0 -95
data/lib/hecks/codemod.rb CHANGED
@@ -2,20 +2,26 @@ require_relative "bluebook/meta_validator"
2
2
  require_relative "corpus"
3
3
 
4
4
  module Hecks
5
- # Shared machinery for a codemod that migrates real `.bluebook` source
6
- # once a DSL builder change makes some previously-required declaration
7
- # optional/redundant — pulled out of `bin/codemod_implicit_append_fields`
8
- # (the first one built), which needed three real, hard-won fixes before
9
- # it could be trusted: a process-lifetime AST cache with no
10
- # invalidation, a batch-revert granularity that let one unsafe
11
- # candidate sink every other safe one sharing its boot, and (in the
12
- # spine the codemod migrates for, not here) an append-at-end insertion
13
- # that only round-tripped correctly for whichever field happened to be
14
- # last. None of those are guessable in advance; they only surface by
15
- # actually running a real edit against real self-hosted code. This
16
- # module is that lesson, kept — the next codemod plugs in two rule-
17
- # specific procs (`find_candidates`, `apply_candidate`) and inherits
18
- # the boot/safety-net machinery rather than rediscovering it.
5
+ # Shared machinery for a codemod that migrates real `.bluebook` source.
6
+ #
7
+ # ## Why this module exists
8
+ #
9
+ # For migrating source once a DSL builder change turns a required
10
+ # declaration into an optional, redundant one — pulled out of
11
+ # `bin/codemod_implicit_append_fields` (the first one built), which
12
+ # needed three real, hard-won fixes before it could be trusted: a
13
+ # process-lifetime AST cache with no invalidation, a batch-revert
14
+ # granularity that let one unsafe candidate sink every other safe one
15
+ # sharing its boot, and (in the spine the codemod migrates for, not
16
+ # here) an append-at-end insertion that only round-tripped correctly
17
+ # for whichever field happened to be last. None of those are guessable
18
+ # in advance; they only surface by actually running a real edit
19
+ # against real self-hosted code. This module is that lesson, kept —
20
+ # the next codemod plugs in two rule-specific procs (`find_candidates`,
21
+ # `apply_candidate`) and inherits the boot/safety-net machinery rather
22
+ # than rediscovering it.
23
+ #
24
+ # ## The three-step contract
19
25
  #
20
26
  # **A codemod is not pattern-matching alone**. Deciding "is this line safe
21
27
  # to delete" means knowing what the runtime would resolve it to — so
@@ -33,9 +39,11 @@ module Hecks
33
39
  # can surface as a runtime refusal, not just a differing export)
34
40
  # -> revert and report skipped, never silently guessed past.
35
41
  #
36
- # Design-time checklist for the spine change a future codemod migrates
37
- # corpus text for — both items below are real bugs this module's own
38
- # first use found, not hypothetical:
42
+ # ## Design-time checklist for a future codemod
43
+ #
44
+ # For the spine change a future codemod migrates corpus text for —
45
+ # both items below are real bugs this module's own first use found,
46
+ # not hypothetical:
39
47
  # - Does the resolved value get inserted into an order-sensitive
40
48
  # list (the exported IR is array-order-sensitive throughout)? If
41
49
  # so, the spine's own insertion must preserve the original
@@ -70,6 +78,10 @@ module Hecks
70
78
  MEMORY_ADAPTER = File.join(ROOT, "lib/hecks/adapters/driven/memory.adapter")
71
79
  PRISM_ADAPTER = File.join(ROOT, "lib/hecks/adapters/driven/prism.adapter")
72
80
 
81
+ # Exports a booted registry's canonical IR as JSON, for a before/after diff.
82
+ #
83
+ # @param registry [Runtime::Registry] the booted registry to export
84
+ # @return [String] the pretty-printed JSON IR export
73
85
  def self.export_json(registry) = Hecks::Projector::Exporter.json(registry)
74
86
 
75
87
  # `Hecks::Adapters::Prism` caches a file's parsed AST for the
@@ -83,6 +95,10 @@ module Hecks
83
95
  # file. `Prism.forget` is the real invalidation API this module's
84
96
  # own first use motivated (found here, fixed at the source rather
85
97
  # than left as a private `TREES.clear` poke from outside).
98
+ #
99
+ # @param path [String, Array<String>] a single `.bluebook` file, a directory
100
+ # to glob every `.bluebook` file from, or an explicit list of file paths
101
+ # @return [Runtime::Registry] a fresh registry with every file loaded and judged
86
102
  def self.load_bluebook(path)
87
103
  paths = if path.is_a?(Array)
88
104
  path
@@ -109,12 +125,17 @@ module Hecks
109
125
  # files (`MetaValidator::GRAMMAR_FILES`) merged into one registry,
110
126
  # and a caller here (the codemod runner) doesn't generally know in
111
127
  # advance which one it just edited.
128
+ # @return [String] the freshly re-derived meta-domain's canonical IR, as JSON
112
129
  def self.boot_meta
113
130
  Hecks::Adapters::Prism.forget_all
114
131
  Hecks::Bluebook::MetaValidator.instance_variable_set(:@grammar_registry, nil)
115
132
  export_json(Hecks::Bluebook::MetaValidator.grammar_registry)
116
133
  end
117
134
 
135
+ # Forces the self-hosted meta-domain to re-derive from its current source,
136
+ # discarding any cached parse trees or memoized registry.
137
+ #
138
+ # @return [Runtime::Registry] the freshly re-derived meta-domain's registry
118
139
  def self.meta_registry
119
140
  Hecks::Adapters::Prism.forget_all
120
141
  Hecks::Bluebook::MetaValidator.instance_variable_set(:@grammar_registry, nil)
@@ -128,6 +149,14 @@ module Hecks
128
149
  # `AggregateBuilder#command` vs `EntityBuilder#command` already
129
150
  # draws. Generic enough for any rule that needs to walk real
130
151
  # commands, not specific to the attribute-redundancy rule.
152
+ #
153
+ # @param registry [Runtime::Registry] a booted registry
154
+ # @yield [construct, command] every command in the registry, once per aggregate
155
+ # and once per nested entity
156
+ # @yieldparam construct [Bluebook::Aggregate, Bluebook::Entity] whichever
157
+ # aggregate or entity actually owns `command`
158
+ # @yieldparam command [Bluebook::Command] the command
159
+ # @return [Hash{String => Bluebook::Chapter}] `registry.bluebooks`, unchanged
131
160
  def self.each_command(registry)
132
161
  registry.bluebooks.each_value do |chapter|
133
162
  chapter.aggregates.each do |aggregate|
@@ -140,6 +169,12 @@ module Hecks
140
169
  end
141
170
  end
142
171
 
172
+ # Finds one construct's own attribute by name.
173
+ #
174
+ # @param construct [Bluebook::Aggregate, Bluebook::Entity] the construct to search
175
+ # @param name [String, Symbol, #to_s] the attribute's declared name
176
+ # @return [Bluebook::Attribute, nil] the matching attribute, or nil if `construct`
177
+ # declares no attribute named `name`
143
178
  def self.owner_attribute(construct, name)
144
179
  construct.attributes.find { |attr| attr.name.to_s == name.to_s }
145
180
  end
@@ -149,18 +184,48 @@ module Hecks
149
184
  # `AttributeCollector#resolve_identity_field!` already does. Shared
150
185
  # because "what does this list actually hold" is a question any
151
186
  # append-shaped rule needs answered, not just this one.
187
+ #
188
+ # Nothing in the DSL stops a value object and an entity nested under
189
+ # the same aggregate from sharing a `hecks_name` — they are declared
190
+ # through two separate collections with no cross-kind uniqueness
191
+ # check between them. A silent value-objects-before-entities pick
192
+ # would resolve such a pair the same way every time regardless of
193
+ # which one `list_field` actually names, so a genuine collision is
194
+ # refused instead, the same "never first-wins silently" call
195
+ # `EntityBuilder#install_closed_sets!` already makes for a same-named
196
+ # closed set.
197
+ #
198
+ # @param construct [Bluebook::Aggregate, Bluebook::Entity] the construct
199
+ # declaring `list_field`
200
+ # @param list_field [String, Symbol, #to_s] the name of the `list_of(...)` attribute
201
+ # @return [Bluebook::Entity, Bluebook::ValueObject, nil] the construct
202
+ # `list_field` holds a list of, or nil if `list_field` names no attribute,
203
+ # isn't a list, or names no known value object or entity
204
+ # @raise [RuntimeError] if a value object and an entity both match `list_field`'s type
152
205
  def self.element_construct_for(construct, list_field)
153
206
  list_attr = owner_attribute(construct, list_field)
154
207
  return nil unless list_attr&.list?
155
208
 
156
- pool = construct.respond_to?(:value_objects) ? construct.value_objects.dup : []
157
- pool.concat(construct.entities) if construct.respond_to?(:entities)
158
- pool.find { |c| c.hecks_name.to_s == list_attr.type.to_s }
209
+ value_objects = construct.respond_to?(:value_objects) ? construct.value_objects : []
210
+ entities = construct.respond_to?(:entities) ? construct.entities : []
211
+ matches = (value_objects + entities).select { |c| c.hecks_name.to_s == list_attr.type.to_s }
212
+
213
+ if matches.size > 1
214
+ raise "#{construct.hecks_name}##{list_field} names #{list_attr.type}, held by both a value " \
215
+ "object and an entity — ambiguous, cannot resolve which one the list holds"
216
+ end
217
+
218
+ matches.first
159
219
  end
160
220
 
161
221
  # Either a raised exception or a differing export counts as unsafe
162
222
  # — see the module header on why the meta-domain specifically can
163
223
  # raise. Returns [value_or_nil, error_message_or_nil].
224
+ #
225
+ # @yield the risky boot/export step to run
226
+ # @return [Array(Object, nil), Array(nil, String)] `[the block's result, nil]`
227
+ # on success, or `[nil, "ExceptionClass: message"]` if the block raises
228
+ # any StandardError
164
229
  def self.safely
165
230
  [yield, nil]
166
231
  rescue StandardError => e
@@ -186,12 +251,29 @@ module Hecks
186
251
  # label: ->(candidate) { "..." }
187
252
  # One-line description for the results report.
188
253
  class Runner
254
+ # @param find_candidates [Proc] `->(registry) { [...] }` — given a booted
255
+ # registry, returns every candidate this rule could migrate; a candidate's
256
+ # shape is the caller's own, read back only by `apply_candidate`
257
+ # @param apply_candidate [Proc] `->(text, candidate) { [new_text, changed_bool] }`
258
+ # — given one file's current text and one candidate, returns the edited text
259
+ # and whether a match was found; `false` means the candidate doesn't apply to
260
+ # this file
261
+ # @param label [Proc] `->(candidate) { "..." }` — a one-line description of a
262
+ # candidate, for the results report
189
263
  def initialize(find_candidates:, apply_candidate:, label:)
190
264
  @find_candidates = find_candidates
191
265
  @apply_candidate = apply_candidate
192
266
  @label = label
193
267
  end
194
268
 
269
+ # Runs this rule's codemod across every example domain and the meta-domain.
270
+ #
271
+ # @param dry_run [Boolean] when true, every safe edit is still written and
272
+ # reverified, then reverted rather than kept
273
+ # @return [Hash{Symbol => Array}] `:applied` (`Array<Hash{file: String,
274
+ # candidates: Array<String>}>`), `:skipped` (`Array<Hash{file: String,
275
+ # reason: String, candidates: Array<String>}>`), and `:clean`
276
+ # (`Array<String>` of domain directories or `"meta-domain"` with no candidates)
195
277
  def run(dry_run: false)
196
278
  results = { applied: [], skipped: [], clean: [] }
197
279
  run_example_domains(results, dry_run)
@@ -199,6 +281,11 @@ module Hecks
199
281
  results
200
282
  end
201
283
 
284
+ # Prints `run`'s results to stdout.
285
+ #
286
+ # @param results [Hash{Symbol => Array}] a `run` result
287
+ # @param dry_run [Boolean] whether this was a dry run, for the report's own heading
288
+ # @return [void]
202
289
  def report(results, dry_run:)
203
290
  puts "== results (#{dry_run ? 'DRY RUN — nothing written' : 'applied'}) =="
204
291
  puts "clean (no candidates): #{results[:clean].join(', ')}" unless results[:clean].empty?
@@ -1,6 +1,8 @@
1
1
  module Hecks
2
2
  # The invisible field a built construct carries.
3
3
  #
4
+ # ## Identity
5
+ #
4
6
  # A construct is a record with an owner chain — the chapter (Bluebook)
5
7
  # owns its aggregates, an aggregate owns everything declared on it — and the
6
8
  # bluebook identity is carried in its own field, under a `hecks_` prefix that
@@ -20,7 +22,7 @@ module Hecks
20
22
  # record of that construct carry the same identity, and there is no
21
23
  # translation table between them to be quietly wrong in.
22
24
  #
23
- # Usage:
25
+ # ## Usage
24
26
  #
25
27
  # price = Class.new(ValueObject) # a declaration holder
26
28
  # price.hecks_name = "Price"
@@ -41,14 +43,21 @@ module Hecks
41
43
  # rather than a top — see hecks_fqn.
42
44
  attr_writer :hecks_root
43
45
 
46
+ # Whether this construct is the top of its own owner chain.
47
+ #
48
+ # @return [Boolean] true for a chapter, which sets `hecks_root`; false otherwise
44
49
  def hecks_root? = @hecks_root ? true : false
45
50
 
46
51
  # The name as the bluebook declares it, never the constant path.
52
+ #
53
+ # @return [String] the construct's own name, without any owner prefix
47
54
  def hecks_name = @hecks_name
48
55
 
49
56
  # How this construct joins its owner. An aggregate is a member of its
50
57
  # chapter's namespace (`::`) ; everything else is declared on its owner
51
58
  # (`.`). Overridden by Aggregate, defaulted here for every other construct.
59
+ #
60
+ # @return [String] `"."`, the separator this construct uses in `hecks_fqn`
52
61
  def hecks_separator = "."
53
62
 
54
63
  # Refuses rather than guesses. A construct with no owner and no claim to be a
@@ -56,6 +65,11 @@ module Hecks
56
65
  # while entities are still IR objects — and answering the bare name would be a
57
66
  # plausible half-truth that no test would notice. That shape of falsehood is
58
67
  # what this repo keeps finding, so it goes red instead.
68
+ #
69
+ # @return [String] the fully-qualified name, joining every owner from the
70
+ # chapter down to this construct
71
+ # @raise [Construct::Unowned] if this construct has no owner and is not itself
72
+ # a chapter (root)
59
73
  def hecks_fqn
60
74
  return hecks_name.to_s if hecks_root?
61
75
 
data/lib/hecks/corpus.rb CHANGED
@@ -4,14 +4,13 @@ module Hecks
4
4
  # **The corpus, discovered** — every place in this repo that holds a real
5
5
  # domain, named once.
6
6
  #
7
- # This used to be spelled out separately by every consumer that walks
8
- # it — spec/corpus_spec.rb, spec/model_check_spec.rb, bin/model_check,
9
- # spec/parser_parity_spec.rb, bin/fuzz, Fuzzing::CombinationMiner — and
10
- # the copies had already drifted: only the model checker saw
7
+ # One table here, rather than each consumer spelling out its own copy —
8
+ # spec/corpus_spec.rb, spec/model_check_spec.rb, bin/model_check,
9
+ # spec/parser_parity_spec.rb, bin/fuzz, Fuzzing::CombinationMiner all did,
10
+ # and the copies had already drifted: only the model checker saw
11
11
  # `qa/bluebook`, only parser parity saw `spec/fixtures`, and nothing but
12
- # bin/fuzz's own sweep ever saw `qa/stress_domains`. One table here, and
13
- # each consumer names the KINDS it walks rather than re-deriving where
14
- # those kinds live.
12
+ # bin/fuzz's own sweep ever saw `qa/stress_domains`. Each consumer names
13
+ # the `KINDS` it walks rather than re-deriving where those kinds live.
15
14
  #
16
15
  # Plain Dir/File only — bin/ scripts require this before (or without)
17
16
  # booting anything.
@@ -25,7 +24,19 @@ module Hecks
25
24
  DIRECTORY_KINDS = {
26
25
  example: "examples/*",
27
26
  stress: "qa/stress_domains/*",
28
- semantics: "spec/corpus/semantics/domains/*"
27
+ semantics: "spec/corpus/semantics/domains/*",
28
+ # A PACKAGE VENDORED VIA `uses_embryonaut_bluebook` — same real
29
+ # mechanism `uses_framework` is (`lib/hecks/embryonaut_bluebook.rb`'s
30
+ # own header: "same shape as Framework"), one level further out: not
31
+ # a chapter shipped inside THIS gem's own `lib/hecks/framework/
32
+ # bluebook/` (the `:framework` FILE_KIND, below), but a directory
33
+ # nested inside the CONSUMING example's own checkout
34
+ # (`<domain>/vendor/embryonaut_bluebooks/<name>/bluebook/`,
35
+ # EmbryonautBluebook.load!'s own resolution path) — hence its own
36
+ # DIRECTORY_KIND rather than reuse of `:framework`'s shape. First
37
+ # real member: docs/decisions/0058's own
38
+ # examples/embryonaut_vendoring_demo/vendor/embryonaut_bluebooks/widgets.
39
+ vendored: "examples/*/vendor/embryonaut_bluebooks/*"
29
40
  }.freeze
30
41
 
31
42
  # **One chapter per file**. Stemmed by the path below the glob's fixed
@@ -37,6 +48,7 @@ module Hecks
37
48
  qa: "qa/bluebook/*.bluebook",
38
49
  language: "lib/hecks/language/**/*.bluebook",
39
50
  deploy: "lib/hecks/deploy/bluebook/*.bluebook",
51
+ tenancy: "lib/hecks/tenancy/bluebook/*.bluebook",
40
52
  fixture: "spec/fixtures/**/*.bluebook"
41
53
  }.freeze
42
54
 
@@ -88,6 +100,13 @@ module Hecks
88
100
 
89
101
  module_function
90
102
 
103
+ # Lists corpus members of the given kinds (every kind, by default).
104
+ #
105
+ # @param kinds [Array<Symbol>] corpus kinds to include, from `KINDS`; every kind
106
+ # when empty
107
+ # @param root [String] repository root to search under
108
+ # @return [Array<Member>] matching members
109
+ # @raise [ArgumentError] if `kinds` names a kind not in `KINDS`
91
110
  def members(*kinds, root: ROOT)
92
111
  kinds = KINDS if kinds.empty?
93
112
  kinds.flat_map do |kind|
@@ -107,6 +126,10 @@ module Hecks
107
126
  # What a boot loads for a member: a directory kind's bluebook
108
127
  # directory, a file kind's own file. `nil` for a directory holding no
109
128
  # bluebook at all.
129
+ #
130
+ # @param member [Member] the corpus member
131
+ # @return [String, nil] the path to boot, or nil when a directory member holds
132
+ # no bluebook
110
133
  def source_of(member)
111
134
  DIRECTORY_KINDS.key?(member.kind) ? bluebook_dir(member.path) : member.path
112
135
  end
@@ -114,6 +137,9 @@ module Hecks
114
137
  # Where a domain path keeps its bluebooks — `<domain>/bluebook/*.bluebook`
115
138
  # (every example and stress domain), or the directory itself
116
139
  # (`qa/bluebook`). `nil` when neither holds a bluebook.
140
+ #
141
+ # @param domain_path [String] path to a domain directory
142
+ # @return [Array<String>, nil] `.bluebook` file paths found, or nil when none
117
143
  def bluebook_files(domain_path)
118
144
  [File.join(domain_path, "bluebook"), domain_path].each do |dir|
119
145
  files = Dir[File.join(dir, "*.bluebook")]
@@ -122,6 +148,11 @@ module Hecks
122
148
  nil
123
149
  end
124
150
 
151
+ # The directory holding `domain_path`'s bluebooks.
152
+ #
153
+ # @param domain_path [String] path to a domain directory
154
+ # @return [String, nil] directory of the first `.bluebook` file found, or nil
155
+ # when `domain_path` holds none
125
156
  def bluebook_dir(domain_path)
126
157
  files = bluebook_files(domain_path)
127
158
  files && File.dirname(files.first)
@@ -129,6 +160,9 @@ module Hecks
129
160
 
130
161
  # The route a repo-relative path takes instead of the sweep — the
131
162
  # first one it matches — or `nil` when the sweep boots it.
163
+ #
164
+ # @param relative_path [String] a corpus member's path, relative to the repo root
165
+ # @return [Route, nil] the first matching route, or nil when the sweep boots it
132
166
  def route_for(relative_path)
133
167
  ROUTES.find { |route| route.pattern.match?(relative_path) }
134
168
  end
@@ -139,8 +173,12 @@ module Hecks
139
173
  # member a route already sends to a destination of its own: the
140
174
  # broken-on-purpose model_check fixtures must produce their findings
141
175
  # there, so a clean-corpus gate here would be the wrong check for them.
142
- MODEL_CHECK_KINDS = %i[example grammar framework qa stress fixture].freeze
176
+ MODEL_CHECK_KINDS = %i[example grammar framework vendored qa stress fixture].freeze
143
177
 
178
+ # Every corpus member `bin/model_check` and `spec/model_check_spec.rb` walk.
179
+ #
180
+ # @param root [String] repository root to search under
181
+ # @return [Array<Member>] members of `MODEL_CHECK_KINDS`, less any routed elsewhere
144
182
  def model_check_members(root: ROOT)
145
183
  members(*MODEL_CHECK_KINDS, root: root).reject { |member| route_for(member.path.delete_prefix("#{root}/")) }
146
184
  end
@@ -162,6 +200,10 @@ module Hecks
162
200
  # puts a domain in the rotation and nothing tied that list to the
163
201
  # corpus. Promotion only ever printed the `target.identify` line for a
164
202
  # human to run.
203
+ #
204
+ # @param root [String] repository root to search under
205
+ # @return [Hash{String => String}] each rotation member's stem/reference mapped
206
+ # to its repo-relative path
165
207
  def rotation_targets(root: ROOT)
166
208
  members(:example, :stress, root: root)
167
209
  .to_h { |member| [member.stem, member.path.delete_prefix("#{root}/")] }
@@ -171,6 +213,9 @@ module Hecks
171
213
  # Every bootable domain in the project, not a hand-kept list — any
172
214
  # directory holding a `.bluebook` no route sends elsewhere, a
173
215
  # `bluebook/` folder standing for the domain directory around it.
216
+ #
217
+ # @param root [String] repository root to search under
218
+ # @return [Array<String>] absolute paths of every sweepable domain directory
174
219
  def sweepable_domains(root = ROOT)
175
220
  Dir.chdir(root) do
176
221
  Dir.glob("**/*.bluebook")
@@ -184,6 +229,9 @@ module Hecks
184
229
 
185
230
  # The domain directory a member stands for, spelled the way
186
231
  # `sweepable_domains` spells it: a `bluebook/` folder is its parent.
232
+ #
233
+ # @param member [Member] the corpus member
234
+ # @return [String] the member's owning domain directory path
187
235
  def domain_dir_of(member)
188
236
  dir = File.directory?(member.path) ? member.path : File.dirname(member.path)
189
237
  File.basename(dir) == "bluebook" ? File.dirname(dir) : dir
@@ -192,10 +240,10 @@ module Hecks
192
240
  # ── The Rust-facing corpus ─────────────────────────────────────────
193
241
  #
194
242
  # Every Rust-facing list (the fuzz bridge, the codegen drift check,
195
- # rust coverage, codegen parity) used to be typed out by hand, and
196
- # each had drifted from `rust/Cargo.toml`'s `[features]`: fuzzing saw
197
- # 8 of 20 features. These read the one source instead. The
198
- # `rust/src/generated/` modules split into buckets, and
243
+ # rust coverage, codegen parity) reads `rust/Cargo.toml`'s `[features]`
244
+ # as its one source, rather than being typed out by hand per consumer
245
+ # and drifting from it — hand-typed, fuzzing once saw only 8 of 20
246
+ # features. The `rust/src/generated/` modules split into buckets, and
199
247
  # spec/corpus_rust_spec.rb proves every Cargo feature and every
200
248
  # generated module lands in exactly one of them:
201
249
  #
@@ -232,15 +280,27 @@ module Hecks
232
280
  # it is deleted here.
233
281
  RUST_COVERAGE_PENDING = {}.freeze
234
282
 
283
+ # The Cargo `[features]` table's raw text.
284
+ #
285
+ # @param root [String] repository root to search under
286
+ # @return [String] the table's text, or `""` when `rust/Cargo.toml` has none
235
287
  def cargo_features_table(root: ROOT)
236
288
  File.read(File.join(root, "rust/Cargo.toml"))[Fuzzing::TargetCapabilities::FEATURES_TABLE] || ""
237
289
  end
238
290
 
291
+ # Every feature name Cargo declares with no dependency array of its own.
292
+ #
293
+ # @param root [String] repository root to search under
294
+ # @return [Array<String>] feature names, in file order
239
295
  def cargo_features(root: ROOT)
240
296
  cargo_features_table(root: root).scan(/^(\w+)\s*=\s*\[\]/).flatten
241
297
  end
242
298
 
243
299
  # The feature `bin/project_rust` last wrote as Cargo's `default`.
300
+ #
301
+ # @param root [String] repository root to search under
302
+ # @return [String, nil] the default feature's name, or nil when Cargo.toml
303
+ # declares none
244
304
  def cargo_default(root: ROOT)
245
305
  cargo_features_table(root: root)[/^default\s*=\s*\["(\w+)"\]/, 1]
246
306
  end
@@ -250,6 +310,9 @@ module Hecks
250
310
  # decides which directory it came from — a directory name alone is not
251
311
  # enough: spec/fixtures/qa_discover_external_domains vendors a second
252
312
  # `examples/pizzas` that no Cargo feature was ever generated from.
313
+ #
314
+ # @param root [String] repository root to search under
315
+ # @return [Array<RustDomain>] each Rust-facing domain, sorted by directory path
253
316
  def rust_domains(root: ROOT)
254
317
  features = cargo_features(root: root)
255
318
  members(*RUST_DOMAIN_KINDS, root: root)
@@ -260,6 +323,12 @@ module Hecks
260
323
  .map { |dir, kind| RustDomain.new(File.basename(dir).downcase, dir, kind) }
261
324
  end
262
325
 
326
+ # Tells whether `dir` is the real source directory for its Cargo feature.
327
+ #
328
+ # @param dir [String] a candidate domain directory
329
+ # @param features [Array<String>] known Cargo feature names
330
+ # @param root [String] repository root `dir` is rooted under
331
+ # @return [Boolean]
263
332
  def rust_domain_dir?(dir, features, root)
264
333
  feature = File.basename(dir).downcase
265
334
  source = generated_source(feature, root: root)
@@ -269,36 +338,83 @@ module Hecks
269
338
  # Where a generated module came from, read off the stamp bin/project_rust
270
339
  # writes into its metadata.rs — `examples/pizzas`, `/abs/path/embryonaut`,
271
340
  # `the self-hosted language (lib/hecks/language/bluebook)`, with any
272
- # ` (uses_framework "X")` suffix dropped. `nil` when not generated.
273
- SOURCE_STAMP = %r{GENERATED by bin/project_rust — (.+?)(?: \(uses_framework "\w+"\))?'s own canonical IR,}
274
-
341
+ # ` (uses_framework "X")` or ` (uses_embryonaut_bluebook "X")` suffix
342
+ # dropped. `nil` when not generated.
343
+ SOURCE_STAMP = %r{
344
+ GENERATED\ by\ bin/project_rust\ —\ (.+?)
345
+ (?:\ \((?:uses_framework|uses_embryonaut_bluebook)\ "\w+"\))?
346
+ 's\ own\ canonical\ IR,
347
+ }x
348
+
349
+ # Where a generated module came from, read off the stamp `bin/project_rust`
350
+ # writes into its metadata.rs.
351
+ #
352
+ # @param module_name [String] a generated module's name
353
+ # @param root [String] repository root to search under
354
+ # @return [String, nil] the source path the stamp names, or nil when the module
355
+ # is not generated
275
356
  def generated_source(module_name, root: ROOT)
276
357
  metadata = File.join(root, GENERATED_DIR, module_name, "metadata.rs")
277
358
  File.file?(metadata) ? File.read(metadata)[SOURCE_STAMP, 1] : nil
278
359
  end
279
360
 
361
+ # Every module directory under `rust/src/generated`.
362
+ #
363
+ # @param root [String] repository root to search under
364
+ # @return [Array<String>] generated module names, sorted
280
365
  def generated_modules(root: ROOT)
281
366
  Dir.children(File.join(root, GENERATED_DIR)).select { |name| File.directory?(File.join(root, GENERATED_DIR, name)) }.sort
282
367
  end
283
368
 
369
+ # Tells whether `feature` has already been generated (has a merged.rs).
370
+ #
371
+ # @param feature [String] a Cargo feature / domain name
372
+ # @param root [String] repository root to search under
373
+ # @return [Boolean]
284
374
  def generated?(feature, root: ROOT)
285
375
  File.file?(File.join(root, GENERATED_DIR, feature, "merged.rs"))
286
376
  end
287
377
 
288
- # Framework members `bin/project_rust` writes a module for as a side
289
- # effect of some `uses_framework` domain — a module, no merged.rs.
290
- def rust_framework_chapters(root: ROOT)
378
+ # Chapters attached via `kind` (`:framework` or `:vendored`) that
379
+ # `bin/project_rust` writes a module for as a side effect of the
380
+ # attaching domain — a module, no merged.rs of its own. Both kinds
381
+ # share this shape: `bin/project_rust`'s own generated module name is
382
+ # always `chapter_name.downcase` (rust/src/generated/mod.rs's own
383
+ # convention), and for either kind that downcased name is always the
384
+ # original lowercase directory stem again — confirmed live for a
385
+ # framework member and for a vendored package alike (a vendored
386
+ # package's own bluebook stem doesn't have to equal its declared
387
+ # chapter name the way a framework member's does — `EmbryonautBluebook.
388
+ # load!` resolves it as `Naming.pascal(dir_name)`, e.g. "widgets" ->
389
+ # "Widgets" — but downcasing a `Naming.pascal`-derived name always
390
+ # returns the original lowercase stem again).
391
+ #
392
+ # @param kind [Symbol] :framework or :vendored
393
+ # @param root [String] repository root to search under
394
+ # @return [Array<String>] stems with a generated module but no merged.rs
395
+ # of their own
396
+ def rust_side_chapters(kind, root: ROOT)
291
397
  modules = generated_modules(root: root)
292
- members(:framework, root: root).map(&:stem)
293
- .select { |stem| modules.include?(stem) && !generated?(stem, root: root) }
398
+ members(kind, root: root).map(&:stem)
399
+ .select { |stem| modules.include?(stem) && !generated?(stem, root: root) }
294
400
  end
295
401
 
296
- # The regeneration order the drift check runs. Sorted by path, so
402
+ def rust_framework_chapters(root: ROOT)
403
+ rust_side_chapters(:framework, root: root)
404
+ end
405
+
406
+ def rust_vendored_chapters(root: ROOT)
407
+ rust_side_chapters(:vendored, root: root)
408
+ end
409
+
410
+ # THE REGENERATION ORDER the drift check runs. Sorted by path, so
297
411
  # which domain runs last — and so wins Cargo's `default`, mod.rs's cfg
298
412
  # comments and the shared framework modules' attribution stamp — is a
299
- # fact of the sorted list, not a hand-picked order. (The old hand list
300
- # put waybill last; PR #667 moved to the sort, which makes
301
- # has_many_fixture last.) spec/corpus_rust_spec.rb pins last == default.
413
+ # fact of the sorted list, not a hand-picked order: today that is
414
+ # `has_many_fixture`. spec/corpus_rust_spec.rb pins last == default.
415
+ #
416
+ # @param root [String] repository root to search under
417
+ # @return [Array<RustDomain>] already-generated Rust domains, in regeneration order
302
418
  def rust_regen_order(root: ROOT)
303
419
  rust_domains(root: root).select { |domain| generated?(domain.feature, root: root) }
304
420
  end
@@ -308,6 +424,11 @@ module Hecks
308
424
  # file is named after its role, `aggregate.bluebook`, while its
309
425
  # chapter is always "Bluebook"). Scans the whole file: a framework
310
426
  # member's header comment can run past any fixed line cap.
427
+ #
428
+ # @param bluebook_path [String, Array<String>] a `.bluebook` file path, or an
429
+ # array whose first element is used
430
+ # @return [String, nil] the declared chapter name, or nil when the file never
431
+ # declares one
311
432
  def chapter_name_of(bluebook_path)
312
433
  bluebook_path = Array(bluebook_path).first
313
434
  header = File.foreach(bluebook_path).find { |line| line =~ /\A\s*Hecks\.bluebook\s+"([^"]+)"/ }
@@ -1,4 +1,23 @@
1
1
  # The Deploy domain, bound so it boots standing alone.
2
2
  Hecks.hecksagon "Deploy" do
3
3
  persisted_by "Memory"
4
+
5
+ # THE IMPERATIVE HALF, BEHIND A REAL PORT — `bin/project_tenant`
6
+ # dispatches this `asks` once `Tenant.Declare` has already validated
7
+ # the request; `TenantProvisioner` (lib/hecks/adapters/driven/
8
+ # tenant_provisioner.rb) writes the overlay file and answers or
9
+ # refuses. `database`/`adapter`/`directory` are declared here, not on
10
+ # `Tenant` itself — CLI-only facts this validated request never
11
+ # needed to hold as its own state. `directory` is the real
12
+ # filesystem path being provisioned — NOT re-derivable from `Tenant`'s
13
+ # own `domain` (its declared NAME, e.g. "Scratch", never a path).
14
+ Deploy::Tenant.port "TenantProvisioning" do
15
+ asks "Provision", to: Tenant do
16
+ attribute :database, DatabaseName
17
+ attribute :adapter, AdapterName
18
+ attribute :directory, DirectoryPath
19
+ answers "TenantProvisioned"
20
+ refuses "ProvisioningRefused"
21
+ end
22
+ end
4
23
  end