hecks 2.6.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. checksums.yaml +4 -4
  2. data/exe/hecks +13 -0
  3. data/lib/hecks/adapters/driven/google_authentication.rb +1 -1
  4. data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
  5. data/lib/hecks/adapters/driven/memory.rb +1 -1
  6. data/lib/hecks/adapters/driven/postgres.rb +1 -1
  7. data/lib/hecks/adapters/driven/prism.rb +2 -2
  8. data/lib/hecks/adapters/driven/sqlite/projection.rb +1 -1
  9. data/lib/hecks/adapters/driven/tenant_provisioner.rb +31 -10
  10. data/lib/hecks/behaviors/expectations.rb +2 -2
  11. data/lib/hecks/bluebook/assembly/contract.rb +1 -1
  12. data/lib/hecks/bluebook/assembly/contracts.rb +5 -5
  13. data/lib/hecks/bluebook/assembly/specializer.rb +2 -2
  14. data/lib/hecks/bluebook/assembly.rb +1 -1
  15. data/lib/hecks/bluebook/behaviour/chapter.rb +5 -2
  16. data/lib/hecks/bluebook/behaviour/read_model.rb +18 -0
  17. data/lib/hecks/bluebook/capabilities.rb +2 -2
  18. data/lib/hecks/bluebook/chapter.rb +1 -1
  19. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +2 -2
  20. data/lib/hecks/bluebook/dsl/binding_proxy.rb +6 -1
  21. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +2 -2
  22. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -0
  23. data/lib/hecks/bluebook/dsl/command_builder.rb +3 -3
  24. data/lib/hecks/bluebook/dsl/entity_builder.rb +1 -1
  25. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +1 -1
  26. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +14 -9
  27. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +1 -1
  28. data/lib/hecks/bluebook/dsl/rule_reference.rb +3 -3
  29. data/lib/hecks/bluebook/dsl/translation_builder.rb +1 -1
  30. data/lib/hecks/bluebook/dsl/world_builder.rb +47 -3
  31. data/lib/hecks/bluebook/hexagon.rb +15 -7
  32. data/lib/hecks/bluebook/meta_validator/judge.rb +3 -3
  33. data/lib/hecks/bluebook/meta_validator/plan.rb +4 -4
  34. data/lib/hecks/bluebook/meta_validator/readings.rb +1 -1
  35. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +2 -2
  36. data/lib/hecks/bluebook/meta_validator/shapes.rb +1 -1
  37. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +11 -10
  38. data/lib/hecks/bluebook/meta_validator/world_judge.rb +3 -1
  39. data/lib/hecks/bluebook/meta_validator.rb +1 -1
  40. data/lib/hecks/bluebook/model_check/client_profile.rb +4 -70
  41. data/lib/hecks/bluebook/model_check.rb +13 -14
  42. data/lib/hecks/cache_dir.rb +103 -0
  43. data/lib/hecks/cli/document.rb +104 -0
  44. data/lib/hecks/cli/ir.rb +47 -0
  45. data/lib/hecks/cli/mcp.rb +32 -0
  46. data/lib/hecks/cli/mcp_door.rb +461 -0
  47. data/lib/hecks/cli/model_check.rb +270 -0
  48. data/lib/hecks/cli/project_cli.rb +123 -0
  49. data/lib/hecks/cli/project_diagrams.rb +43 -0
  50. data/lib/hecks/cli/run.rb +219 -0
  51. data/lib/hecks/cli/smoke_test.rb +59 -0
  52. data/lib/hecks/cli/stores.rb +54 -0
  53. data/lib/hecks/cli.rb +166 -0
  54. data/lib/hecks/embryonaut_bluebook/lock.rb +72 -0
  55. data/lib/hecks/embryonaut_bluebook/shape.rb +47 -0
  56. data/lib/hecks/embryonaut_bluebook/vendor.rb +194 -0
  57. data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +88 -0
  58. data/lib/hecks/embryonaut_bluebook.rb +29 -3
  59. data/lib/hecks/facade/handle.rb +2 -2
  60. data/lib/hecks/facade/surface/aggregate_door.rb +1 -1
  61. data/lib/hecks/forms/value_object_shape.rb +1 -1
  62. data/lib/hecks/framework/bluebook/console_settings.bluebook +3 -3
  63. data/lib/hecks/language/bluebook/vocabulary.bluebook +16 -0
  64. data/lib/hecks/language/world/world.bluebook +32 -2
  65. data/lib/hecks/mcp_door_scope.rb +176 -0
  66. data/lib/hecks/ports/key_vault.rb +1 -1
  67. data/lib/hecks/ports/persistence/append_only.rb +1 -1
  68. data/lib/hecks/ports/persistence/binding_policy.rb +16 -5
  69. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -2
  70. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +2 -2
  71. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +2 -2
  72. data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +132 -0
  73. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +1 -1
  74. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -2
  75. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +1 -1
  76. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +1 -1
  77. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +13 -7
  78. data/lib/hecks/ports/persistence/plugins/era.rb +7 -4
  79. data/lib/hecks/ports/persistence/postgres_dump/connection.rb +73 -0
  80. data/lib/hecks/ports/persistence/postgres_dump.rb +125 -0
  81. data/lib/hecks/ports/persistence/repository_factory.rb +2 -2
  82. data/lib/hecks/ports/persistence/state_codec.rb +1 -1
  83. data/lib/hecks/ports/persistence.rb +4 -0
  84. data/lib/hecks/projections/deploy/fargate/assembly.rb +193 -0
  85. data/lib/hecks/projections/deploy/fargate/cdn.rb +318 -0
  86. data/lib/hecks/projections/deploy/fargate/cdn_default.rb +87 -0
  87. data/lib/hecks/projections/deploy/fargate/check.rb +172 -0
  88. data/lib/hecks/projections/deploy/fargate/containers.rb +412 -0
  89. data/lib/hecks/projections/deploy/fargate/extras.rb +268 -0
  90. data/lib/hecks/projections/deploy/fargate/monitoring.rb +448 -0
  91. data/lib/hecks/projections/deploy/fargate/settings.rb +235 -0
  92. data/lib/hecks/projections/deploy/fargate/yaml.rb +116 -0
  93. data/lib/hecks/projections/deploy/fargate.rb +188 -139
  94. data/lib/hecks/projections/deploy/lambda.rb +8 -8
  95. data/lib/hecks/projections/deploy/preview/access_yaml.rb +145 -0
  96. data/lib/hecks/projections/deploy/preview/containers.rb +218 -0
  97. data/lib/hecks/projections/deploy/preview/preview.sh.erb +446 -0
  98. data/lib/hecks/projections/deploy/preview/routing_yaml.rb +216 -0
  99. data/lib/hecks/projections/deploy/preview/script.rb +40 -0
  100. data/lib/hecks/projections/deploy/preview/settings.rb +140 -0
  101. data/lib/hecks/projections/deploy/preview/task_yaml.rb +215 -0
  102. data/lib/hecks/projections/deploy/preview/template.rb +247 -0
  103. data/lib/hecks/projections/deploy/preview/yaml_text.rb +48 -0
  104. data/lib/hecks/projections/deploy/preview.rb +103 -0
  105. data/lib/hecks/projections/deploy/scripts/deploy-service.sh.tmpl +182 -0
  106. data/lib/hecks/projections/deploy/scripts/expected-era.header +17 -0
  107. data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +54 -0
  108. data/lib/hecks/projections/deploy/scripts/settings.rb +114 -0
  109. data/lib/hecks/projections/deploy/scripts/smoke-after-deploy.sh.tmpl +232 -0
  110. data/lib/hecks/projections/deploy/scripts.rb +171 -0
  111. data/lib/hecks/projections/deploy/smoke/harness.js +366 -0
  112. data/lib/hecks/projections/deploy/smoke/workflow.yml.tmpl +56 -0
  113. data/lib/hecks/projections/deploy/smoke.rb +159 -0
  114. data/lib/hecks/projections/deploy/template_diff/comparer.rb +231 -0
  115. data/lib/hecks/projections/deploy/template_diff/loader.rb +105 -0
  116. data/lib/hecks/projections/deploy/template_diff.rb +173 -0
  117. data/lib/hecks/projections/glossary/sensitivity.rb +1 -1
  118. data/lib/hecks/projections/reference.rb +7 -1
  119. data/lib/hecks/projections.rb +2 -0
  120. data/lib/hecks/projector/exporter.rb +4 -4
  121. data/lib/hecks/projector/target.rb +1 -1
  122. data/lib/hecks/release/gem_pin/published_versions.rb +58 -0
  123. data/lib/hecks/release/gem_pin.rb +112 -0
  124. data/lib/hecks/release/runner/ci_publisher.rb +96 -0
  125. data/lib/hecks/release/runner/commands.rb +68 -0
  126. data/lib/hecks/release/runner/console.rb +52 -0
  127. data/lib/hecks/release/runner/gem_publisher.rb +44 -0
  128. data/lib/hecks/release/runner/git.rb +48 -0
  129. data/lib/hecks/release/runner/npm_publisher.rb +89 -0
  130. data/lib/hecks/release/runner/preflight.rb +119 -0
  131. data/lib/hecks/release/runner/published.rb +55 -0
  132. data/lib/hecks/release/runner/tagger.rb +87 -0
  133. data/lib/hecks/release/runner.rb +244 -0
  134. data/lib/hecks/router/namespace_installer.rb +1 -1
  135. data/lib/hecks/runtime/command_interpreter.rb +3 -3
  136. data/lib/hecks/runtime/command_rules/admissibility.rb +2 -2
  137. data/lib/hecks/runtime/command_rules/arithmetic.rb +2 -2
  138. data/lib/hecks/runtime/entity_element.rb +1 -1
  139. data/lib/hecks/runtime/entity_interpreter.rb +1 -1
  140. data/lib/hecks/runtime/invocation.rb +1 -1
  141. data/lib/hecks/runtime/loader.rb +27 -3
  142. data/lib/hecks/runtime/outbox.rb +1 -1
  143. data/lib/hecks/runtime/reaction_invocation.rb +3 -3
  144. data/lib/hecks/runtime/read_model_interpreter.rb +37 -15
  145. data/lib/hecks/runtime/reference_hop.rb +4 -4
  146. data/lib/hecks/runtime/registry/saga_persistence.rb +8 -2
  147. data/lib/hecks/runtime/registry/verification.rb +15 -13
  148. data/lib/hecks/runtime/registry/world_defaults.rb +106 -0
  149. data/lib/hecks/runtime/registry.rb +22 -13
  150. data/lib/hecks/runtime/saga_interpreter.rb +1 -1
  151. data/lib/hecks/runtime/tenant_check.rb +1 -1
  152. data/lib/hecks/runtime/value/coercion.rb +3 -3
  153. data/lib/hecks/runtime/value/entity_list_coercion.rb +1 -1
  154. data/lib/hecks/storehouse.rb +18 -12
  155. data/lib/hecks/vendoring/git_environment.rb +35 -0
  156. data/lib/hecks/vendoring/git_source.rb +102 -0
  157. data/lib/hecks/vendoring.rb +102 -0
  158. data/lib/hecks/version.rb +1 -1
  159. data/lib/hecks/vocabulary.rb +5 -0
  160. data/lib/hecks.rb +8 -2
  161. metadata +72 -58
  162. data/lib/hecks/bench/cli.rb +0 -74
  163. data/lib/hecks/bench/environment.rb +0 -100
  164. data/lib/hecks/bench/postgres_probe.rb +0 -41
  165. data/lib/hecks/bench/report.rb +0 -116
  166. data/lib/hecks/bench/ruby_runner.rb +0 -151
  167. data/lib/hecks/bench/run.rb +0 -55
  168. data/lib/hecks/bench/rust_runner.rb +0 -165
  169. data/lib/hecks/bench/stats.rb +0 -75
  170. data/lib/hecks/bench/suite.rb +0 -172
  171. data/lib/hecks/bench/workload.rb +0 -137
  172. data/lib/hecks/bench.rb +0 -28
  173. data/lib/hecks/codemod.rb +0 -443
  174. data/lib/hecks/corpus.rb +0 -479
  175. data/lib/hecks/doc/reference.rb +0 -585
  176. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -684
  177. data/lib/hecks/fuzzing/combination_miner.rb +0 -178
  178. data/lib/hecks/fuzzing/concurrent_dispatch.rb +0 -469
  179. data/lib/hecks/fuzzing/coverage_campaign.rb +0 -161
  180. data/lib/hecks/fuzzing/differential.rb +0 -192
  181. data/lib/hecks/fuzzing/domain_generator.rb +0 -871
  182. data/lib/hecks/fuzzing/era_boundary.rb +0 -154
  183. data/lib/hecks/fuzzing/form_census.rb +0 -285
  184. data/lib/hecks/fuzzing/generated_domain_check.rb +0 -171
  185. data/lib/hecks/fuzzing/invalid_value_generator.rb +0 -125
  186. data/lib/hecks/fuzzing/isolated_boot.rb +0 -604
  187. data/lib/hecks/fuzzing/nondeterministic.rb +0 -79
  188. data/lib/hecks/fuzzing/persistence_parity.rb +0 -253
  189. data/lib/hecks/fuzzing/properties/corrections.rb +0 -125
  190. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +0 -688
  191. data/lib/hecks/fuzzing/properties/guards.rb +0 -398
  192. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +0 -399
  193. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +0 -137
  194. data/lib/hecks/fuzzing/properties/outbox.rb +0 -180
  195. data/lib/hecks/fuzzing/properties/querying.rb +0 -260
  196. data/lib/hecks/fuzzing/properties.rb +0 -267
  197. data/lib/hecks/fuzzing/qa_settings.rb +0 -164
  198. data/lib/hecks/fuzzing/replay.rb +0 -930
  199. data/lib/hecks/fuzzing/rotation_priority.rb +0 -114
  200. data/lib/hecks/fuzzing/rust_gap_manifest.rb +0 -139
  201. data/lib/hecks/fuzzing/self_consistency.rb +0 -825
  202. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +0 -532
  203. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +0 -171
  204. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +0 -127
  205. data/lib/hecks/fuzzing/sequence_generator/picker.rb +0 -116
  206. data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +0 -138
  207. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +0 -344
  208. data/lib/hecks/fuzzing/sequence_generator.rb +0 -311
  209. data/lib/hecks/fuzzing/shrinker.rb +0 -309
  210. data/lib/hecks/fuzzing/structural_skips.rb +0 -53
  211. data/lib/hecks/fuzzing/sweep_depth.rb +0 -61
  212. data/lib/hecks/fuzzing/target_capabilities.rb +0 -255
  213. data/lib/hecks/fuzzing/value_generator.rb +0 -340
  214. data/lib/hecks/fuzzing.rb +0 -23
  215. data/lib/hecks/grammar/evolve.rb +0 -524
  216. data/lib/hecks/query_ir.rb +0 -482
@@ -56,7 +56,13 @@ module Hecks
56
56
  end
57
57
  end
58
58
 
59
- # Walks every loaded domain, repopulating `saga_instances` from
59
+ # Every domain whose sagas can persist somewhere: one with a hecksagon, or one a
60
+ # world's `default_adapter` binds though it has none.
61
+ #
62
+ # @return [Array<String>] the domain names, hecksagon domains first
63
+ def saga_domains = @hecksagons.keys | @bluebooks.keys.select { |domain| default_adapter_for(domain) }
64
+
65
+ # Walks every loaded domain in `saga_domains`, repopulating `saga_instances` from
60
66
  # whatever `saga_persistence(domain)` resolves to — a real store
61
67
  # for a domain whose adapter answers the capability, `each_saga`
62
68
  # yielding real rows; `NULL_SAGA_STORE`'s own `each_saga` for
@@ -79,7 +85,7 @@ module Hecks
79
85
  #
80
86
  # @return [Hecks::Runtime::Registry] self
81
87
  def rehydrate_sagas!
82
- @hecksagons.each_key do |domain|
88
+ saga_domains.each do |domain|
83
89
  saga_persistence(domain).each_saga do |process_manager, correlation, state, memory, completed_compensations = []|
84
90
  pending = memory.delete(SAGA_PENDING_DISPATCH_KEY)
85
91
  @saga_instances[process_manager][correlation] =
@@ -17,10 +17,12 @@ module Hecks
17
17
  # @raise [Runtime::WiringError] if a bind names an undeclared aggregate, an
18
18
  # adapter cannot satisfy its port's verb or declared `answers`, a world
19
19
  # setting names a field its adapter does not declare, the default adapter
20
- # is unusable, a command declares a role with no authorization provider
21
- # attached, or a membership chapter is loaded with no identity chapter
20
+ # is unusable, a world's `default_adapter` is not a persistence adapter, a
21
+ # command declares a role with no authorization provider attached, or a
22
+ # membership chapter is loaded with no identity chapter
22
23
  def verify!
23
24
  verify_default_adapter!
25
+ verify_world_defaults!
24
26
  verify_singleton_port_answers!
25
27
  refuse_cross_package_bluebook_merge!
26
28
  refuse_membership_without_identity!
@@ -270,7 +272,7 @@ module Hecks
270
272
  # types the consumer inlines. `uses_embryonaut_bluebook` only loads
271
273
  # the package's `.bluebook` files — persistence, Governance, and the
272
274
  # anti-corruption layer (`translates` field mapping) live on a named
273
- # sibling hecksagon the CONSUMER must declare. Without that hexagon
275
+ # sibling hecksagon the consumer must declare. Without that hexagon
274
276
  # the package has no wiring of its own, and cross-context field
275
277
  # mapping has nowhere to be written. Breaking in 2.0: attaching a
276
278
  # vendored chapter without `Hecks.hecksagon "PackageName"` refuses
@@ -309,7 +311,7 @@ module Hecks
309
311
  # An explicit `bounded` mark on a consumer chapter always needs an
310
312
  # ACL — at least one `translates` block. Any field can be mapped;
311
313
  # the BC does not list which. `uses_framework` / `uses_embryonaut_bluebook`
312
- # mark the ATTACHED chapter bounded (module wrap, no Object shortcut)
314
+ # mark the attached chapter bounded (module wrap, no Object shortcut)
313
315
  # and require the sibling hecksagon above; they do not require a
314
316
  # `translates` on that sibling unless the consumer also wrote `bounded`.
315
317
  # rust/host Google sign-in reads `ir.json`'s `membership` and
@@ -391,22 +393,22 @@ module Hecks
391
393
  bluebook_ir.aggregates.flat_map { |aggregate| aggregate.commands + aggregate.entities.flat_map(&:commands) }
392
394
  end
393
395
 
394
- # TWO UNRELATED PACKAGES, ONE CHAPTER NAME BY COINCIDENCE — the
396
+ # Two unrelated packages, one chapter name by coincidence — the
395
397
  # real risk `Registry#bluebook_sources` exists to catch (found
396
398
  # live: a stale `vendor/hecksagain` fork's own copy of Governance/
397
399
  # Identity/Deploy, still reachable on 4 consuming apps' own load
398
400
  # paths alongside the real gem). `BluebookBuilder.build`'s own
399
- # accumulation (several files declaring the SAME chapter name ON
400
- # PURPOSE — `lib/hecks/language/bluebook/*.bluebook` all open
401
+ # accumulation (several files declaring the same chapter name on
402
+ # purpose — `lib/hecks/language/bluebook/*.bluebook` all open
401
403
  # `Hecks.bluebook "Bluebook"`) is never touched here — that merge
402
- # stays unconditional, checked only AFTER every file has loaded,
404
+ # stays unconditional, checked only after every file has loaded,
403
405
  # the same "check the merged final result once" timing
404
406
  # `refuse_ungoverned_roles!` already uses and for the same reason
405
407
  # (a check against an incomplete load can never see the real
406
408
  # shape). What distinguishes intentional accumulation from
407
- # coincidence is PACKAGE ROOT, not file identity: files a real
409
+ # coincidence is package root, not file identity: files a real
408
410
  # gemspec or a `vendor/` boundary already treats as one unit are
409
- # expected to share a name; files from two DIFFERENT roots never
411
+ # expected to share a name; files from two different roots never
410
412
  # legitimately do.
411
413
  def refuse_cross_package_bluebook_merge!
412
414
  @bluebook_sources.each do |name, paths|
@@ -422,9 +424,9 @@ module Hecks
422
424
  end
423
425
  end
424
426
 
425
- # THE NEAREST BOUNDARY A PATH ALREADY BELONGS TO — a real
426
- # gemspec (this IS a package, whatever depends on it or vendors
427
- # it), or a bare `vendor/` path component, treated as its OWN
427
+ # The nearest boundary a path already belongs to — a real
428
+ # gemspec (this is a package, whatever depends on it or vendors
429
+ # it), or a bare `vendor/` path component, treated as its own
428
430
  # root regardless of what gemspec might sit above it: vendored
429
431
  # code should never be considered "the same package" as whatever
430
432
  # it's vendored into, even when nothing else marks the boundary.
@@ -0,0 +1,106 @@
1
+ module Hecks
2
+ module Runtime
3
+ class Registry
4
+ # **World defaults** — the `default_database` and `default_adapter` words a
5
+ # `.world` can declare once for every chapter a boot loaded, resolved against
6
+ # the registry's own worlds. Included into Registry; the callers are the
7
+ # persistence binding policy (`default_adapter_for`), every reader of a bind's
8
+ # world settings (`binding_settings`), and `verify!` (`verify_world_defaults!`).
9
+ #
10
+ # **Resolution order**, most specific first — nothing here ever overrides a
11
+ # chapter's own explicit declaration:
12
+ #
13
+ # 1. the chapter's own `persisted_by(...) { database ... }` block, and the
14
+ # hecksagon bind (aggregate-specific, then domain-level) that names its adapter;
15
+ # 2. the default declared by the chapter's own world;
16
+ # 3. the default declared by the project's world — the world of the first
17
+ # chapter the boot loaded, the same convention `Loader#dispatcher_for` uses
18
+ # to find the domain a boot targets;
19
+ # 4. today's behavior: no database setting, and the framework's in-memory
20
+ # adapter for a chapter with no hecksagon.
21
+ #
22
+ # A default is resolved when a bind's settings or binding are asked for, never
23
+ # written into a chapter's world or hecksagon, so a chapter's own declarations
24
+ # stay exactly what its files say and a hecksagon-less chapter still trips the
25
+ # sibling-hecksagon gates the way it always did.
26
+ module WorldDefaults
27
+ # The settings a chapter's world gives one bind, with the project's default
28
+ # database filled in where the chapter left it out.
29
+ #
30
+ # The default database only ever reaches a persistence bind whose adapter
31
+ # declares a `database` field — a projection or an adapter without one is
32
+ # untouched. A `database` the chapter's own settings name wins; every other
33
+ # setting the chapter declared rides along unchanged.
34
+ #
35
+ # @param domain [String, Symbol] the domain whose world is asked
36
+ # @param verb [String, Symbol] the bind's port verb, such as `"persisted_by"`
37
+ # @param adapter [String, Symbol] the bind's adapter name
38
+ # @return [Hash{Symbol => Object}] the chapter's own settings for the bind, plus
39
+ # `:database` from the default when one applies; `{}` when there is nothing
40
+ def binding_settings(domain, verb, adapter)
41
+ declared = world(domain)&.for_binding(verb, adapter) || {}
42
+ database = default_database_for(domain)
43
+ return declared unless database && default_database_applies?(verb, adapter)
44
+
45
+ { adapter: adapter.to_s, database: database }.merge(declared)
46
+ end
47
+
48
+ # The persistence adapter a chapter's aggregates fall back to when their
49
+ # hecksagon does not bind them.
50
+ #
51
+ # @param domain [String, Symbol] the domain to resolve the default for
52
+ # @return [String, nil] the chapter's own world's `default_adapter`, else the
53
+ # project's; `nil` when neither declares one
54
+ def default_adapter_for(domain) = world_default(domain, :default_adapter)
55
+
56
+ # The database connection a chapter's database-taking persistence adapter uses
57
+ # when its own settings name none.
58
+ #
59
+ # @param domain [String, Symbol] the domain to resolve the default for
60
+ # @return [String, nil] the chapter's own world's `default_database`, else the
61
+ # project's; `nil` when neither declares one
62
+ def default_database_for(domain) = world_default(domain, :default_database)
63
+
64
+ # Refuses a `default_adapter` that no persistence adapter answers to.
65
+ #
66
+ # @return [Runtime::Registry] self
67
+ # @raise [Runtime::WiringError] if a world's `default_adapter` names an unknown
68
+ # adapter, one that is not a persistence adapter, or one with no Ruby
69
+ # implementation
70
+ def verify_world_defaults!
71
+ @worlds.each_value do |declared|
72
+ name = declared.default_adapter
73
+ next unless name
74
+
75
+ verify_default_adapter_named!(declared.domain, name)
76
+ end
77
+ self
78
+ end
79
+
80
+ private
81
+
82
+ # The chapter's own world answers first; the project's world fills the gap.
83
+ def world_default(domain, field)
84
+ world(domain)&.public_send(field) || project_world&.public_send(field)
85
+ end
86
+
87
+ # The world of the first chapter this registry loaded — the target of a boot, ahead of
88
+ # any framework member it attaches.
89
+ def project_world = world(@bluebooks.keys.first || @worlds.keys.first)
90
+
91
+ def default_database_applies?(verb, adapter)
92
+ verb.to_s == Ports::Persistence::VERB && @adapters[adapter.to_s]&.declares?(:database)
93
+ end
94
+
95
+ def verify_default_adapter_named!(domain, name)
96
+ check_verb(Bluebook::Bind.new(aggregate: "(default)", verb: Ports::Persistence::VERB, adapter: name))
97
+ adapter_class(name)
98
+ rescue WiringError => e
99
+ raise WiringError,
100
+ "#{domain}'s world declares default_adapter #{name.inspect}, which is not a usable " \
101
+ "persistence adapter: #{e.message}"
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -1,5 +1,6 @@
1
1
  require_relative "registry/verification"
2
2
  require_relative "registry/saga_persistence"
3
+ require_relative "registry/world_defaults"
3
4
  require_relative "outbox"
4
5
  require_relative "../naming"
5
6
 
@@ -10,10 +11,12 @@ module Hecks
10
11
  # The collections a boot gathers — bluebooks, hexagons, ports, adapters,
11
12
  # worlds, the logs — and how a repository is resolved from them. The
12
13
  # wiring gate lives in registry/verification.rb, saga persistence
13
- # resolution in registry/saga_persistence.rb.
14
+ # resolution in registry/saga_persistence.rb, and a `.world`'s project-wide
15
+ # defaults in registry/world_defaults.rb.
14
16
  class Registry
15
17
  include Verification
16
18
  include SagaPersistence
19
+ include WorldDefaults
17
20
 
18
21
  attr_reader :root, :bluebooks, :hecksagons, :ports, :adapters, :worlds, :event_log,
19
22
  :reaction_log, :saga_log, :saga_instances, :translations, :saga_mutex,
@@ -36,7 +39,7 @@ module Hecks
36
39
  @event_log = []
37
40
  @reaction_log = []
38
41
  @saga_log = []
39
- # A DECLARATIVE FACT, NOT YET A DISPATCHED ONE — `AggregateDoor#
42
+ # A declarative fact, not yet a dispatched one — `AggregateDoor#
40
43
  # mark_sensitive` (called from a `.hecksagon` file, the same way
41
44
  # `port`/`persisted_by` already are) appends here at
42
45
  # hecksagon-build time; `Runtime::Loader.boot`'s own post-dispatcher
@@ -159,19 +162,23 @@ module Hecks
159
162
  # @return [Bluebook::Chapter] `item`, unchanged
160
163
  def add_bluebook(item) = @bluebooks[item.name] = item
161
164
 
162
- # PROVENANCE, SIDE-CHANNEL — which real `.bluebook` file(s)
165
+ # Provenance, side-channel — which real `.bluebook` file(s)
163
166
  # contributed to a chapter name, never part of the exported IR (a
164
167
  # boot-time loading fact, not a domain fact) and never Rust-mirrored
165
168
  # (the same "additive, Ruby-only" shape `@translations` above already
166
- # is). Legitimate accumulation (several files declaring the SAME
169
+ # is). Legitimate accumulation (several files declaring the same
167
170
  # chapter name on purpose — `lib/hecks/language/bluebook/*.bluebook`
168
171
  # all open `Hecks.bluebook "Bluebook"`) pushes more than one path
169
172
  # here too; that alone is not a problem. What this exists to let
170
173
  # `refuse_cross_package_bluebook_merge!` (registry/verification.rb)
171
- # catch is TWO UNRELATED PACKAGES accumulating into the same name by
174
+ # catch is two unrelated packages accumulating into the same name by
172
175
  # coincidence — a stale vendored fork's own copy of a real gem's
173
176
  # chapter, still reachable on the load path, silently merging its
174
177
  # aggregates into the real one via this exact accumulation mechanism.
178
+ #
179
+ # @param name [String, Symbol] the chapter name the file contributed to
180
+ # @param path [String] the `.bluebook` file that declared it
181
+ # @return [Array<String>] every path recorded for that chapter, `path` last
175
182
  def record_bluebook_source(name, path)
176
183
  (@bluebook_sources[name.to_s] ||= []) << path
177
184
  end
@@ -257,7 +264,7 @@ module Hecks
257
264
  # entry into a real `Privacy::Marking.Mark` once a dispatcher exists.
258
265
  #
259
266
  # @param domain [String] the marked attribute's own aggregate FQN, e.g.
260
- # `"Lifeadelics::Registration"`
267
+ # `"Site::Registration"`
261
268
  # @param attribute_path [String] the dotted path within that aggregate, e.g.
262
269
  # `"attendee.medications"`
263
270
  # @param category [String] the marking's own sensitivity category, e.g. `"phi"`
@@ -593,7 +600,7 @@ module Hecks
593
600
  # concatenated, `base` then `overlay`
594
601
  def merge_hecksagons(base, overlay)
595
602
  # Same-name blocks concatenate regardless of which file they came
596
- # from (`lifeadelics.hecksagon`, `context_map.hecksagon`, an
603
+ # from (`site.hecksagon`, `context_map.hecksagon`, an
597
604
  # environment overlay). Order-independent: list facts uniq, so
598
605
  # loading context_map before or after the domain file is the same
599
606
  # merged hecksagon. Binds stay concatenated (BindingPolicy still
@@ -610,8 +617,8 @@ module Hecks
610
617
  end
611
618
 
612
619
  # Recovered and generalized — see `add_world`'s own comment. `realm`/
613
- # `latest` are scalars, so the overlay's value wins when present,
614
- # else the base's survives; `settings` is a shallow merge keyed by
620
+ # `latest`, `default_database` and `default_adapter` are scalars, so the overlay's
621
+ # value wins when present, else the base's survives; `settings` is a shallow merge keyed by
615
622
  # verb (and `"verb:adapter"`) — an overlay entry for a key the base
616
623
  # also declares replaces that key's whole resolved hash (the same
617
624
  # all-or-nothing shape `WorldBuilder#method_missing` already builds
@@ -623,10 +630,12 @@ module Hecks
623
630
  # present, and `settings` shallow-merged, `overlay`'s keys winning
624
631
  def merge_worlds(base, overlay)
625
632
  Bluebook::World.new(
626
- domain: base.domain,
627
- realm: overlay.realm || base.realm,
628
- latest: overlay.latest || base.latest,
629
- settings: base.settings.merge(overlay.settings)
633
+ domain: base.domain,
634
+ realm: overlay.realm || base.realm,
635
+ latest: overlay.latest || base.latest,
636
+ settings: base.settings.merge(overlay.settings),
637
+ default_database: overlay.default_database || base.default_database,
638
+ default_adapter: overlay.default_adapter || base.default_adapter
630
639
  )
631
640
  end
632
641
  end
@@ -425,7 +425,7 @@ module Hecks
425
425
  #
426
426
  # A compensation that is itself refused does not unwind again, and needs no
427
427
  # flag to stop it: the state moves to the compensating leg's to_state before
428
- # its dispatches run, so a second refusal finds the instance no longer in
428
+ # its dispatches run, so a second refusal finds the instance already out of
429
429
  # from_state and records that instead. The check is the guard.
430
430
  def unwind(process_manager, event, instance, correlation, domain)
431
431
  return unless instance && process_manager.handles?(REFUSED)
@@ -38,7 +38,7 @@ module Hecks
38
38
  # search_path` — proven for real, not assumed, by
39
39
  # tenant_isolation_spec.rb. Plain Postgres (no schema story) and D1
40
40
  # (no schema-equivalent at all — see world.bluebook's own comment on
41
- # the lifeadelics D1 tradeoff) answer false, or don't answer at all,
41
+ # the D1 tradeoff) answer false, or don't answer at all,
42
42
  # which this module treats identically to false.
43
43
  module TenantCheck
44
44
  module_function
@@ -916,11 +916,11 @@ module Hecks
916
916
  # C3.8 — the boundary check for an attribute whose type is a bare
917
917
  # primitive rather than a value object: `Integer`/`Float` by exact
918
918
  # numeric class (`NUMERIC`), `String`/booleans by rejecting only a
919
- # composite shape (`COMPOSITE_SHAPES`) — not the same as
919
+ # composite shape (`COMPOSITE_SHAPES`) — a looser test than
920
920
  # `check_scalar_shapes` holds a value object's own `String` field to
921
- # any more (QualityControl BUG#125 tightened that one to also refuse
921
+ # (QualityControl BUG#125 has that one also refuse
922
922
  # a non-string scalar; a bare `String` argument here still admits
923
- # any other scalar, left exactly as it was — a bare-primitive
923
+ # any other scalar — a bare-primitive
924
924
  # attribute was never part of BUG#125's own investigation or fix,
925
925
  # and whether it needs the same tightening, and against what real
926
926
  # Judge dependency if any, is still open); worded by the same
@@ -27,7 +27,7 @@ module Hecks
27
27
  # because coercion has to resolve value objects, and only the
28
28
  # root answers `.value_object` at all (Entity's own header
29
29
  # comment: an entity must not answer to it, or `Value.
30
- # for_attribute` could no longer tell a piece from a head). So
30
+ # for_attribute` could not tell a piece from a head). So
31
31
  # a nested entity — Dispatch, inside Handler — is not a direct
32
32
  # child of the root the way Handler itself is, and a plain
33
33
  # `aggregate.entities.find` stops one level short of it.
@@ -1,6 +1,7 @@
1
1
  require "json"
2
2
  require "fileutils"
3
3
  require "time"
4
+ require_relative "cache_dir"
4
5
  require_relative "facade/command_request"
5
6
  require_relative "facade/json_door"
6
7
  require_relative "naming"
@@ -8,7 +9,7 @@ require_relative "projector"
8
9
  require_relative "runtime/errors"
9
10
 
10
11
  module Hecks
11
- # **The bus, not a door** — `docs/hecks-survey-what-we-wish-we-had.md` and
12
+ # **The bus, not a door** — `docs/archive/hecks-survey-what-we-wish-we-had.md` and
12
13
  # `docs/future-features.md` both name the sibling project's own
13
14
  # "Storehouse" the single highest-priority gap this repo had: "no
14
15
  # per-command tool... the bluebook is the contract, the [door] just
@@ -90,19 +91,24 @@ module Hecks
90
91
  module Storehouse
91
92
  module_function
92
93
 
93
- # The same closed set `docs/hecks-survey-what-we-wish-we-had.md`'s
94
+ # The same closed set `docs/archive/hecks-survey-what-we-wish-we-had.md`'s
94
95
  # `SourceTag` names — who dispatched, not what. Optional: a caller
95
96
  # that omits it gets `source: nil` recorded, honestly, rather than a
96
97
  # guessed default.
97
98
  SOURCE_TAGS = %w[process-manager operator hook sidequest-agent cascade daemon].freeze
98
99
 
99
- # **The bus's own audit trail** — a JSONL file per domain, one line per
100
- # `dispatch`/`query`/`state`/dry-run call, `follow` tails it back.
101
- # `tmp/`, not the domain's own directory: this is the bus's record
102
- # of what was asked of it, not part of the domain's own persisted
103
- # state, and `tmp/` is already gitignored for exactly this kind of
104
- # local, disposable-but-useful-while-it-lasts file.
105
- LOG_ROOT = File.expand_path("../../tmp/storehouse", __dir__)
100
+ # Names the directory holding the bus's own audit trail.
101
+ #
102
+ # The trail is a JSONL file per domain, one line per
103
+ # `dispatch`/`query`/`state`/dry-run call, and `follow` tails it back.
104
+ # It is kept under `Hecks::CacheDir`, not the domain's own directory: this is
105
+ # the bus's record of what was asked of it, not part of the domain's own
106
+ # persisted state, and not the gem's own directory either, which is
107
+ # read-only on an installed gem.
108
+ #
109
+ # @return [String] the absolute path of the `storehouse` directory under
110
+ # the cache root
111
+ def log_root = Hecks::CacheDir.path("storehouse")
106
112
 
107
113
  # The root every `domain:`/`under:` must resolve under — the project
108
114
  # directory by default, `HECKS_STOREHOUSE_ROOT` to widen or move it.
@@ -321,9 +327,9 @@ module Hecks
321
327
  #
322
328
  # @param domain_name [String, Symbol, #to_s] the domain name
323
329
  # @return [String] the absolute path to that domain's JSONL log file, under
324
- # `LOG_ROOT`
330
+ # `log_root`
325
331
  def log_path(domain_name)
326
- File.join(LOG_ROOT, "#{domain_name.to_s.gsub(/[^A-Za-z0-9_-]/, '_')}.jsonl")
332
+ File.join(log_root, "#{domain_name.to_s.gsub(/[^A-Za-z0-9_-]/, '_')}.jsonl")
327
333
  end
328
334
 
329
335
  # Appends one call's outcome to its domain's audit log.
@@ -351,7 +357,7 @@ module Hecks
351
357
  entry = { time: Time.now.utc.iso8601, tool: tool, verb: verb, summary: summary, source: source,
352
358
  role: role, actor_id: actor_id,
353
359
  ok: outcome[:ok], id: outcome[:id], error: outcome[:error], events: outcome[:events] }.compact
354
- FileUtils.mkdir_p(LOG_ROOT)
360
+ FileUtils.mkdir_p(log_root)
355
361
  File.open(log_path(domain_name), "a") { |f| f.puts(JSON.generate(entry)) }
356
362
  rescue StandardError
357
363
  nil
@@ -0,0 +1,35 @@
1
+ module Hecks
2
+ module Vendoring
3
+ # The environment a `git` subprocess needs so it resolves its repository
4
+ # from the directory it is pointed at, not from whoever launched Ruby.
5
+ #
6
+ # Git exports `GIT_DIR`, `GIT_INDEX_FILE` and their kin to the hooks it
7
+ # runs (a push from a linked worktree runs `pre-push` this way). Every
8
+ # `git` that a hook's process tree starts inherits them, and they outrank
9
+ # both `git -C` and the working directory: a command aimed at a scratch
10
+ # repository would read, or commit into, the repository that was pushing.
11
+ module GitEnvironment
12
+ # The variables that pin git to one repository, work tree or index.
13
+ INHERITED = %w[
14
+ GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_PREFIX GIT_COMMON_DIR
15
+ GIT_OBJECT_DIRECTORY GIT_ALTERNATE_OBJECT_DIRECTORIES
16
+ ].freeze
17
+
18
+ # An environment hash for `Open3`, `IO.popen` and `system` that unsets
19
+ # every repository-pinning variable and leaves the rest alone.
20
+ #
21
+ # @return [Hash{String => nil}] each inherited variable mapped to nil
22
+ def self.clean
23
+ INHERITED.to_h { |name| [name, nil] }
24
+ end
25
+
26
+ # Removes the repository-pinning variables from this process's own
27
+ # environment, so every later `git` child starts clean.
28
+ #
29
+ # @return [void]
30
+ def self.scrub!
31
+ INHERITED.each { |name| ENV.delete(name) }
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,102 @@
1
+ require "open3"
2
+ require "tmpdir"
3
+ require_relative "git_environment"
4
+
5
+ module Hecks
6
+ module Vendoring
7
+ # A local git repository read through the `git` executable, never through
8
+ # its working tree.
9
+ #
10
+ # Everything here names a commit and reads what that commit holds, so a
11
+ # shared checkout with uncommitted edits, or a different branch checked
12
+ # out, cannot leak into what gets vendored. Every command runs under
13
+ # {GitEnvironment.clean}, so a `GIT_DIR` inherited from a hook cannot
14
+ # redirect it to some other repository.
15
+ class GitSource
16
+ # @param path [String] a directory that is a git repository (or a
17
+ # worktree of one)
18
+ # @raise [Vendoring::Error] if `path` is not a directory
19
+ def initialize(path)
20
+ @path = File.expand_path(path.to_s)
21
+ raise Error, "no source repository at #{@path}" unless File.directory?(@path)
22
+ end
23
+
24
+ # @return [String] the absolute path of the repository
25
+ attr_reader :path
26
+
27
+ # Resolves a commit-ish to the full commit id it names.
28
+ #
29
+ # @param ref [String] a branch, tag, abbreviated or full commit id
30
+ # @return [String] the 40-character commit id
31
+ # @raise [Vendoring::Error] if `ref` names no commit in the repository
32
+ def commit(ref)
33
+ out, _err, status = git("rev-parse", "--verify", "--quiet", "#{ref}^{commit}")
34
+ raise Error, "no commit #{ref.inspect} in #{@path}" unless status.success?
35
+
36
+ out.strip
37
+ end
38
+
39
+ # Lists tag names matching a shell pattern.
40
+ #
41
+ # @param pattern [String] a `git tag --list` pattern, such as `"payments-v*"`
42
+ # @return [Array<String>] the matching tag names, in git's own order
43
+ def tags(pattern)
44
+ out, _err, status = git("tag", "--list", pattern)
45
+ status.success? ? out.lines.map(&:chomp) : []
46
+ end
47
+
48
+ # Reads one file as a commit holds it.
49
+ #
50
+ # @param ref [String] the commit-ish to read from
51
+ # @param file [String] a repository-relative path
52
+ # @return [String, nil] the file's content, or nil when the commit has no such file
53
+ def read(ref, file)
54
+ out, _err, status = git("show", "#{ref}:#{file}")
55
+ status.success? ? out : nil
56
+ end
57
+
58
+ # Lists the files directly inside a directory of a commit.
59
+ #
60
+ # Subdirectories are left out on purpose: a vendored package carries the
61
+ # top-level files of its subtree and nothing nested beneath.
62
+ #
63
+ # @param ref [String] the commit-ish to read from
64
+ # @param subtree [String] a repository-relative directory, such as `"payments/bluebook"`
65
+ # @param glob [String] a `File.fnmatch` pattern the file's own name must match
66
+ # @return [Array<String>] repository-relative paths, sorted
67
+ def files(ref, subtree, glob = "*")
68
+ out, _err, status = git("ls-tree", "-r", "--name-only", ref, "--", subtree)
69
+ return [] unless status.success?
70
+
71
+ out.lines.map(&:chomp).select do |file|
72
+ File.dirname(file) == subtree && File.fnmatch(glob, File.basename(file))
73
+ end.sort
74
+ end
75
+
76
+ # Extracts named files of a commit into a directory with `git archive`,
77
+ # keeping their repository-relative paths.
78
+ #
79
+ # @param ref [String] the commit-ish to export
80
+ # @param files [Array<String>] repository-relative paths to export
81
+ # @param into [String] an existing directory to extract into
82
+ # @return [void]
83
+ # @raise [Vendoring::Error] if `git archive` or `tar` fails
84
+ def export(ref, files, into)
85
+ Dir.mktmpdir("hecks-archive") do |scratch|
86
+ tarball = File.join(scratch, "export.tar")
87
+ run!("git archive", "git", "-C", @path, "archive", "--format=tar", "--output=#{tarball}", ref, "--", *files)
88
+ run!("tar", "tar", "-xf", tarball, "-C", into)
89
+ end
90
+ end
91
+
92
+ private
93
+
94
+ def git(*) = Open3.capture3(GitEnvironment.clean, "git", "-C", @path, *)
95
+
96
+ def run!(label, *command)
97
+ _out, err, status = Open3.capture3(GitEnvironment.clean, *command)
98
+ raise Error, "#{label} failed: #{err.lines.first.to_s.strip}" unless status.success?
99
+ end
100
+ end
101
+ end
102
+ end