seams 0.1.0 → 0.2.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 (222) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +212 -12
  3. data/README.md +235 -82
  4. data/lib/generators/seams/accounts/accounts_generator.rb +31 -3
  5. data/lib/generators/seams/accounts/templates/app/controllers/memberships_controller.rb.tt +100 -0
  6. data/lib/generators/seams/accounts/templates/app/views/memberships/index.html.erb.tt +52 -0
  7. data/lib/generators/seams/accounts/templates/config/routes.rb.tt +10 -4
  8. data/lib/generators/seams/accounts/templates/lib/concerns/authorization.rb.tt +35 -2
  9. data/lib/generators/seams/accounts/templates/lib/engine.rb.tt +13 -0
  10. data/lib/generators/seams/accounts/templates/spec/runtime/authorization_spec.rb.tt +102 -0
  11. data/lib/generators/seams/accounts/templates/spec/runtime/memberships_flow_spec.rb.tt +145 -0
  12. data/lib/generators/seams/admin/admin_generator.rb +51 -4
  13. data/lib/generators/seams/admin/templates/README.md.tt +33 -3
  14. data/lib/generators/seams/admin/templates/app/controllers/admin/application_controller.rb.tt +156 -8
  15. data/lib/generators/seams/admin/templates/app/dashboards/admin/account_dashboard.rb.tt +9 -1
  16. data/lib/generators/seams/admin/templates/app/dashboards/admin/accounts_membership_dashboard.rb.tt +9 -1
  17. data/lib/generators/seams/admin/templates/app/dashboards/admin/identity_dashboard.rb.tt +6 -0
  18. data/lib/generators/seams/admin/templates/app/dashboards/admin/invitation_dashboard.rb.tt +7 -1
  19. data/lib/generators/seams/admin/templates/app/dashboards/admin/invoice_dashboard.rb.tt +6 -0
  20. data/lib/generators/seams/admin/templates/app/dashboards/admin/lifetime_pass_dashboard.rb.tt +6 -0
  21. data/lib/generators/seams/admin/templates/app/dashboards/admin/notification_dashboard.rb.tt +6 -0
  22. data/lib/generators/seams/admin/templates/app/dashboards/admin/notification_preference_dashboard.rb.tt +6 -0
  23. data/lib/generators/seams/admin/templates/app/dashboards/admin/plan_dashboard.rb.tt +6 -0
  24. data/lib/generators/seams/admin/templates/app/dashboards/admin/subscription_dashboard.rb.tt +6 -0
  25. data/lib/generators/seams/admin/templates/app/dashboards/admin/team_dashboard.rb.tt +12 -2
  26. data/lib/generators/seams/admin/templates/app/dashboards/admin/teams_membership_dashboard.rb.tt +7 -1
  27. data/lib/generators/seams/admin/templates/app/fields/admin/fields/belongs_to.rb.tt +12 -0
  28. data/lib/generators/seams/admin/templates/app/fields/admin/fields/dashboard_option.rb.tt +22 -0
  29. data/lib/generators/seams/admin/templates/app/fields/admin/fields/has_many.rb.tt +12 -0
  30. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/account_policy.rb.tt +4 -0
  31. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/accounts_membership_policy.rb.tt +6 -0
  32. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/application_policy.rb.tt +51 -10
  33. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/identity_policy.rb.tt +4 -0
  34. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/invitation_policy.rb.tt +6 -0
  35. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/invoice_policy.rb.tt +6 -0
  36. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/lifetime_pass_policy.rb.tt +6 -0
  37. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/notification_policy.rb.tt +6 -0
  38. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/notification_preference_policy.rb.tt +6 -0
  39. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/plan_policy.rb.tt +7 -0
  40. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/subscription_policy.rb.tt +6 -0
  41. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/team_policy.rb.tt +6 -0
  42. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/teams_membership_policy.rb.tt +6 -0
  43. data/lib/generators/seams/admin/templates/app/views/seams/admin/application/_index_header.html.erb.tt +57 -0
  44. data/lib/generators/seams/admin/templates/app/views/seams/admin/application/_navigation.html.erb.tt +14 -0
  45. data/lib/generators/seams/admin/templates/config/routes.rb.tt +24 -14
  46. data/lib/generators/seams/admin/templates/lib/concerns/authenticator.rb.tt +3 -0
  47. data/lib/generators/seams/admin/templates/lib/configuration.rb.tt +3 -1
  48. data/lib/generators/seams/admin/templates/spec/runtime/admin_boot_spec.rb.tt +4 -2
  49. data/lib/generators/seams/auth/add_oauth_provider/add_oauth_provider_generator.rb +1 -2
  50. data/lib/generators/seams/auth/auth_generator.rb +1 -2
  51. data/lib/generators/seams/auth/templates/app/controllers/oauth/callbacks_controller.rb.tt +3 -0
  52. data/lib/generators/seams/auth/templates/app/controllers/password_resets_controller.rb.tt +4 -0
  53. data/lib/generators/seams/auth/templates/app/controllers/registrations_controller.rb.tt +5 -0
  54. data/lib/generators/seams/auth/templates/app/controllers/sessions_controller.rb.tt +5 -0
  55. data/lib/generators/seams/auth/templates/db/migrate/create_auth_oauth_providers.rb.tt +5 -1
  56. data/lib/generators/seams/auth/templates/lib/concerns/authentication.rb.tt +12 -1
  57. data/lib/generators/seams/auth/templates/lib/engine.rb.tt +27 -0
  58. data/lib/generators/seams/billing/billing_generator.rb +4 -3
  59. data/lib/generators/seams/billing/templates/README.md.tt +26 -0
  60. data/lib/generators/seams/billing/templates/app/controllers/invoices_controller.rb.tt +2 -0
  61. data/lib/generators/seams/billing/templates/app/controllers/subscriptions_controller.rb.tt +2 -0
  62. data/lib/generators/seams/billing/templates/app/services/invoices/sync_service.rb.tt +8 -7
  63. data/lib/generators/seams/billing/templates/app/services/stripe_service.rb.tt +9 -7
  64. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_handler_base.rb.tt +18 -3
  65. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/subscription_handler_base.rb.tt +7 -21
  66. data/lib/generators/seams/billing/templates/lib/engine.rb.tt +15 -0
  67. data/lib/generators/seams/billing/templates/lib/gateways/stripe.rb.tt +10 -10
  68. data/lib/generators/seams/billing/templates/lib/stripe/client.rb.tt +40 -14
  69. data/lib/generators/seams/billing/templates/lib/stripe/payload.rb.tt +90 -0
  70. data/lib/generators/seams/billing/templates/lib/stripe/webhook_signature.rb.tt +11 -1
  71. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/charge_refunded.json.tt +1 -0
  72. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/checkout_session_completed.json.tt +1 -0
  73. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_created.json.tt +15 -4
  74. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_deleted.json.tt +7 -1
  75. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_trial_will_end.json.tt +18 -1
  76. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_updated.json.tt +26 -5
  77. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_created.json.tt +16 -2
  78. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_finalized.json.tt +16 -2
  79. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_paid.json.tt +17 -2
  80. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_payment_failed.json.tt +16 -2
  81. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_voided.json.tt +16 -2
  82. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/payment_intent_payment_failed.json.tt +1 -0
  83. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/payment_intent_succeeded.json.tt +1 -0
  84. data/lib/generators/seams/billing/templates/spec/gateways/stripe_spec.rb.tt +47 -1
  85. data/lib/generators/seams/billing/templates/spec/runtime/webhook_handlers_spec.rb.tt +39 -1
  86. data/lib/generators/seams/design/design_generator.rb +647 -0
  87. data/lib/generators/seams/design/templates/README.md.tt +129 -0
  88. data/lib/generators/seams/design/templates/app/assets/tailwind/_tokens.css +322 -0
  89. data/lib/generators/seams/design/templates/app/assets/tailwind/themes/_quire.css +42 -0
  90. data/lib/generators/seams/design/templates/app/controllers/design/dashboard_controller.rb.tt +30 -0
  91. data/lib/generators/seams/design/templates/app/controllers/design/guide_controller.rb.tt +28 -0
  92. data/lib/generators/seams/design/templates/app/form_builders/design/form_builder.rb.tt +90 -0
  93. data/lib/generators/seams/design/templates/app/helpers/design/ui_helper.rb.tt +37 -0
  94. data/lib/generators/seams/design/templates/app/views/design/dashboard/index.html.erb.tt +38 -0
  95. data/lib/generators/seams/design/templates/app/views/design/guide/index.html.erb.tt +19 -0
  96. data/lib/generators/seams/design/templates/app/views/layouts/application.html.erb.tt +61 -0
  97. data/lib/generators/seams/design/templates/app/views/layouts/design/guide.html.erb.tt +21 -0
  98. data/lib/generators/seams/design/templates/app/views/ui/_banner.html.erb.tt +10 -0
  99. data/lib/generators/seams/design/templates/app/views/ui/_breadcrumb.html.erb.tt +8 -0
  100. data/lib/generators/seams/design/templates/app/views/ui/_build_row.html.erb.tt +14 -0
  101. data/lib/generators/seams/design/templates/app/views/ui/_button.html.erb.tt +4 -0
  102. data/lib/generators/seams/design/templates/app/views/ui/_card.html.erb.tt +2 -0
  103. data/lib/generators/seams/design/templates/app/views/ui/_chapter_row.html.erb.tt +11 -0
  104. data/lib/generators/seams/design/templates/app/views/ui/_checkbox.html.erb.tt +12 -0
  105. data/lib/generators/seams/design/templates/app/views/ui/_counter.html.erb.tt +2 -0
  106. data/lib/generators/seams/design/templates/app/views/ui/_data_table.html.erb.tt +25 -0
  107. data/lib/generators/seams/design/templates/app/views/ui/_dialog.html.erb.tt +14 -0
  108. data/lib/generators/seams/design/templates/app/views/ui/_diff.html.erb.tt +12 -0
  109. data/lib/generators/seams/design/templates/app/views/ui/_drawer.html.erb.tt +7 -0
  110. data/lib/generators/seams/design/templates/app/views/ui/_empty.html.erb.tt +5 -0
  111. data/lib/generators/seams/design/templates/app/views/ui/_field.html.erb.tt +14 -0
  112. data/lib/generators/seams/design/templates/app/views/ui/_icon.html.erb.tt +2 -0
  113. data/lib/generators/seams/design/templates/app/views/ui/_icon_sprite.html.erb.tt +22 -0
  114. data/lib/generators/seams/design/templates/app/views/ui/_input_group.html.erb.tt +11 -0
  115. data/lib/generators/seams/design/templates/app/views/ui/_kbd.html.erb.tt +4 -0
  116. data/lib/generators/seams/design/templates/app/views/ui/_menu.html.erb.tt +18 -0
  117. data/lib/generators/seams/design/templates/app/views/ui/_meter.html.erb.tt +15 -0
  118. data/lib/generators/seams/design/templates/app/views/ui/_note.html.erb.tt +2 -0
  119. data/lib/generators/seams/design/templates/app/views/ui/_outline.html.erb.tt +9 -0
  120. data/lib/generators/seams/design/templates/app/views/ui/_pagination.html.erb.tt +12 -0
  121. data/lib/generators/seams/design/templates/app/views/ui/_panel.html.erb.tt +2 -0
  122. data/lib/generators/seams/design/templates/app/views/ui/_popover.html.erb.tt +2 -0
  123. data/lib/generators/seams/design/templates/app/views/ui/_radio.html.erb.tt +5 -0
  124. data/lib/generators/seams/design/templates/app/views/ui/_savestate.html.erb.tt +5 -0
  125. data/lib/generators/seams/design/templates/app/views/ui/_segmented.html.erb.tt +7 -0
  126. data/lib/generators/seams/design/templates/app/views/ui/_stepper.html.erb.tt +12 -0
  127. data/lib/generators/seams/design/templates/app/views/ui/_switch.html.erb.tt +6 -0
  128. data/lib/generators/seams/design/templates/app/views/ui/_tag.html.erb.tt +2 -0
  129. data/lib/generators/seams/design/templates/app/views/ui/_toast.html.erb.tt +12 -0
  130. data/lib/generators/seams/design/templates/app/views/ui/_toolbar.html.erb.tt +15 -0
  131. data/lib/generators/seams/design/templates/app/views/ui/previews/_banner.html.erb.tt +8 -0
  132. data/lib/generators/seams/design/templates/app/views/ui/previews/_breadcrumb.html.erb.tt +5 -0
  133. data/lib/generators/seams/design/templates/app/views/ui/previews/_build_row.html.erb.tt +6 -0
  134. data/lib/generators/seams/design/templates/app/views/ui/previews/_button.html.erb.tt +9 -0
  135. data/lib/generators/seams/design/templates/app/views/ui/previews/_card.html.erb.tt +12 -0
  136. data/lib/generators/seams/design/templates/app/views/ui/previews/_chapter_row.html.erb.tt +6 -0
  137. data/lib/generators/seams/design/templates/app/views/ui/previews/_checkbox.html.erb.tt +6 -0
  138. data/lib/generators/seams/design/templates/app/views/ui/previews/_counter.html.erb.tt +6 -0
  139. data/lib/generators/seams/design/templates/app/views/ui/previews/_data_table.html.erb.tt +14 -0
  140. data/lib/generators/seams/design/templates/app/views/ui/previews/_dialog.html.erb.tt +8 -0
  141. data/lib/generators/seams/design/templates/app/views/ui/previews/_diff.html.erb.tt +10 -0
  142. data/lib/generators/seams/design/templates/app/views/ui/previews/_drawer.html.erb.tt +5 -0
  143. data/lib/generators/seams/design/templates/app/views/ui/previews/_empty.html.erb.tt +5 -0
  144. data/lib/generators/seams/design/templates/app/views/ui/previews/_field.html.erb.tt +5 -0
  145. data/lib/generators/seams/design/templates/app/views/ui/previews/_input_group.html.erb.tt +5 -0
  146. data/lib/generators/seams/design/templates/app/views/ui/previews/_kbd.html.erb.tt +5 -0
  147. data/lib/generators/seams/design/templates/app/views/ui/previews/_menu.html.erb.tt +9 -0
  148. data/lib/generators/seams/design/templates/app/views/ui/previews/_meter.html.erb.tt +6 -0
  149. data/lib/generators/seams/design/templates/app/views/ui/previews/_note.html.erb.tt +2 -0
  150. data/lib/generators/seams/design/templates/app/views/ui/previews/_outline.html.erb.tt +8 -0
  151. data/lib/generators/seams/design/templates/app/views/ui/previews/_pagination.html.erb.tt +5 -0
  152. data/lib/generators/seams/design/templates/app/views/ui/previews/_panel.html.erb.tt +5 -0
  153. data/lib/generators/seams/design/templates/app/views/ui/previews/_popover.html.erb.tt +2 -0
  154. data/lib/generators/seams/design/templates/app/views/ui/previews/_radio.html.erb.tt +6 -0
  155. data/lib/generators/seams/design/templates/app/views/ui/previews/_savestate.html.erb.tt +4 -0
  156. data/lib/generators/seams/design/templates/app/views/ui/previews/_segmented.html.erb.tt +15 -0
  157. data/lib/generators/seams/design/templates/app/views/ui/previews/_stepper.html.erb.tt +6 -0
  158. data/lib/generators/seams/design/templates/app/views/ui/previews/_switch.html.erb.tt +5 -0
  159. data/lib/generators/seams/design/templates/app/views/ui/previews/_tag.html.erb.tt +8 -0
  160. data/lib/generators/seams/design/templates/app/views/ui/previews/_toast.html.erb.tt +6 -0
  161. data/lib/generators/seams/design/templates/app/views/ui/previews/_toolbar.html.erb.tt +11 -0
  162. data/lib/generators/seams/design/templates/lib/design/components.rb.tt +22 -0
  163. data/lib/generators/seams/design/templates/lib/design.rb.tt +18 -0
  164. data/lib/generators/seams/design/templates/lib/engine.rb.tt +38 -0
  165. data/lib/generators/seams/design/templates/lib/generators/design/component/component_generator.rb.tt +52 -0
  166. data/lib/generators/seams/design/templates/lib/generators/design/component/templates/component.html.erb.tt +5 -0
  167. data/lib/generators/seams/design/templates/lib/generators/design/component/templates/preview.html.erb.tt +3 -0
  168. data/lib/generators/seams/design/templates/spec/runtime/design_boot_spec.rb.tt +79 -0
  169. data/lib/generators/seams/design/templates/spec/runtime/form_builder_spec.rb.tt +97 -0
  170. data/lib/generators/seams/design/templates/spec/runtime/guide_spec.rb.tt +40 -0
  171. data/lib/generators/seams/design/templates/spec/runtime/ui_components_spec.rb.tt +70 -0
  172. data/lib/generators/seams/engine/engine_generator.rb +6 -0
  173. data/lib/generators/seams/engine/templates/Gemfile.tt +1 -1
  174. data/lib/generators/seams/engine/templates/app/application_controller.rb.tt +6 -0
  175. data/lib/generators/seams/engine/templates/rubocop.yml.tt +9 -0
  176. data/lib/generators/seams/install/install_generator.rb +72 -6
  177. data/lib/generators/seams/install/templates/Dockerfile.tt +2 -2
  178. data/lib/generators/seams/install/templates/bin_seams.tt +7 -4
  179. data/lib/generators/seams/install/templates/ci.yml.tt +18 -8
  180. data/lib/generators/seams/install/templates/deploy.yml.tt +4 -2
  181. data/lib/generators/seams/install/templates/doc/ARCHITECTURE.md.tt +1 -1
  182. data/lib/generators/seams/install/templates/docker-entrypoint.tt +4 -2
  183. data/lib/generators/seams/install/templates/herb.yml.tt +10 -0
  184. data/lib/generators/seams/install/templates/lefthook.yml.tt +25 -0
  185. data/lib/generators/seams/install/templates/ruby-version.tt +1 -1
  186. data/lib/generators/seams/install/templates/seams.rake.tt +13 -1
  187. data/lib/generators/seams/install/templates/strong_migrations.rb.tt +26 -0
  188. data/lib/generators/seams/notifications/notifications_generator.rb +3 -2
  189. data/lib/generators/seams/notifications/templates/app/controllers/preferences_controller.rb.tt +5 -13
  190. data/lib/generators/seams/notifications/templates/lib/engine.rb.tt +12 -0
  191. data/lib/generators/seams/notifications/templates/lib/notifications.rb.tt +1 -0
  192. data/lib/generators/seams/notifications/templates/lib/preferences.rb.tt +36 -0
  193. data/lib/generators/seams/permissions/permissions_generator.rb +97 -0
  194. data/lib/generators/seams/permissions/templates/config/initializers/seams_permissions.rb.tt +35 -0
  195. data/lib/generators/seams/teams/templates/app/controllers/invitations_controller.rb.tt +27 -11
  196. data/lib/generators/seams/teams/templates/app/controllers/teams_controller.rb.tt +5 -13
  197. data/lib/generators/seams/teams/templates/lib/concerns/authorization.rb.tt +2 -4
  198. data/lib/generators/seams/teams/templates/lib/engine.rb.tt +13 -0
  199. data/lib/seams/cli/list.rb +15 -0
  200. data/lib/seams/cli/quality.rb +46 -5
  201. data/lib/seams/cli/resolve.rb +9 -5
  202. data/lib/seams/cli/test_changed.rb +5 -1
  203. data/lib/seams/cli.rb +20 -0
  204. data/lib/seams/configuration.rb +24 -1
  205. data/lib/seams/cops/no_cross_engine_dependency.rb +39 -7
  206. data/lib/seams/cops/no_cross_engine_model_access.rb +79 -1
  207. data/lib/seams/event_registry.rb +19 -0
  208. data/lib/seams/events/adapter.rb +9 -0
  209. data/lib/seams/events/adapters/active_support.rb +4 -0
  210. data/lib/seams/events/publisher.rb +29 -0
  211. data/lib/seams/events.rb +1 -0
  212. data/lib/seams/generators/dummy_app_writer.rb +21 -0
  213. data/lib/seams/generators/follow_up_generator.rb +1 -2
  214. data/lib/seams/generators/host_injector.rb +9 -2
  215. data/lib/seams/generators/splicer.rb +2 -2
  216. data/lib/seams/observability/adapter.rb +22 -0
  217. data/lib/seams/observability.rb +6 -0
  218. data/lib/seams/permission_registry.rb +66 -0
  219. data/lib/seams/permissions.rb +120 -0
  220. data/lib/seams/version.rb +1 -1
  221. data/lib/seams.rb +7 -0
  222. metadata +107 -3
@@ -29,7 +29,7 @@ module RuboCop
29
29
  # would force every cross-engine read of per-request identity /
30
30
  # account / team to go through a host-defined shim, which defeats
31
31
  # the purpose of `CurrentAttributes` as a shared per-request bus.
32
- # The exception is documented in `doc/CURRENT_ATTRIBUTES.md`.
32
+ # The exception is documented in `doc/reference/CURRENT_ATTRIBUTES.md`.
33
33
  class NoCrossEngineModelAccess < Base
34
34
  MSG = "Engine `%<own>s` must not access `%<const>s` directly. " \
35
35
  "Use an event or a %<other>s-exposed concern instead."
@@ -57,6 +57,13 @@ module RuboCop
57
57
  Engine
58
58
  ].freeze
59
59
 
60
+ ASSOCIATION_MACROS = %i[
61
+ belongs_to
62
+ has_one
63
+ has_many
64
+ has_and_belongs_to_many
65
+ ].freeze
66
+
60
67
  def on_const(node)
61
68
  return unless flaggable?(node)
62
69
 
@@ -71,8 +78,79 @@ module RuboCop
71
78
  )
72
79
  end
73
80
 
81
+ # Association macros name the target class via a `class_name:`
82
+ # STRING (`belongs_to :account, class_name: "Accounts::Account"`),
83
+ # which `on_const` cannot see — string literals are not constant
84
+ # nodes, so this exact pattern used to sail through the cop.
85
+ # Flag the option when the named class lives in another engine.
86
+ #
87
+ # `class_name:` given as a constant is already caught by
88
+ # `on_const`, so only string values are flagged here (no double
89
+ # offense). `polymorphic: true` associations are exempt: the
90
+ # target class is decided at runtime by the owning record.
91
+ def on_send(node)
92
+ pair = string_class_name_pair(node)
93
+ return unless pair
94
+
95
+ full_name = pair.value.value.to_s.delete_prefix("::")
96
+ return unless cross_engine_class_name?(full_name)
97
+
98
+ assert_own_engine_configured!
99
+
100
+ add_offense(
101
+ pair,
102
+ message: format(MSG, own: own_engine, const: full_name,
103
+ other: full_name.split("::").first)
104
+ )
105
+ end
106
+
74
107
  private
75
108
 
109
+ # Returns the `class_name: "..."` pair of an association macro
110
+ # when its value is a string literal worth checking; nil
111
+ # otherwise (not an association, no options, polymorphic, or a
112
+ # constant value already covered by `on_const`).
113
+ def string_class_name_pair(node)
114
+ options = association_options(node)
115
+ return nil unless options
116
+ return nil if polymorphic_option?(options)
117
+
118
+ pair = class_name_pair(options)
119
+ return nil unless pair
120
+
121
+ pair.value.str_type? ? pair : nil
122
+ end
123
+
124
+ def association_options(node)
125
+ return nil unless association_macro?(node)
126
+
127
+ options = node.last_argument
128
+ options if options&.hash_type?
129
+ end
130
+
131
+ def association_macro?(node)
132
+ node.receiver.nil? && ASSOCIATION_MACROS.include?(node.method_name)
133
+ end
134
+
135
+ def polymorphic_option?(options)
136
+ options.pairs.any? do |pair|
137
+ pair.key.sym_type? && pair.key.value == :polymorphic && pair.value.true_type?
138
+ end
139
+ end
140
+
141
+ def class_name_pair(options)
142
+ options.pairs.find { |pair| pair.key.sym_type? && pair.key.value == :class_name }
143
+ end
144
+
145
+ def cross_engine_class_name?(full_name)
146
+ parts = full_name.split("::")
147
+ return false if parts.size < 2
148
+ return false unless other_engines.include?(parts.first)
149
+ return false if exposed_concern?(full_name)
150
+
151
+ true
152
+ end
153
+
76
154
  def flaggable?(node)
77
155
  parts = const_parts_under_other_engine(node)
78
156
  return false unless parts
@@ -12,6 +12,18 @@ module Seams
12
12
  @mutex = Mutex.new
13
13
 
14
14
  class << self
15
+ # Declare that an engine emits an event. Call this from the engine
16
+ # that owns the event (typically in its engine.rb), so the event
17
+ # bus, `bin/rails seams:list`, and the orphan-subscription check
18
+ # all know it exists.
19
+ #
20
+ # @param name [String, Symbol] the event name, +resource.action.engine+ form.
21
+ # @param emitted_by [String] the owning engine (e.g. "Billing").
22
+ # @raise [Seams::Events::InvalidEventNameError] if the name is malformed.
23
+ # @raise [Seams::Events::DuplicateEventError] if another engine already owns it.
24
+ # @return [String] the owning engine name.
25
+ # @example
26
+ # Seams::EventRegistry.register("invoice.paid.billing", emitted_by: "Billing")
15
27
  def register(name, emitted_by:)
16
28
  Events.assert_valid_name!(name)
17
29
  name = name.to_s
@@ -29,18 +41,25 @@ module Seams
29
41
  end
30
42
  end
31
43
 
44
+ # @param name [String, Symbol] an event name.
45
+ # @return [Boolean] whether any engine has registered it.
32
46
  def registered?(name)
33
47
  @registry.key?(name.to_s)
34
48
  end
35
49
 
50
+ # @param name [String, Symbol] an event name.
51
+ # @return [String, nil] the owning engine, or nil if unregistered.
36
52
  def emitter_of(name)
37
53
  @registry[name.to_s]
38
54
  end
39
55
 
56
+ # @return [Hash{String=>String}] a copy of the full event => owner map.
40
57
  def all
41
58
  @registry.dup
42
59
  end
43
60
 
61
+ # Clear the registry. Intended for test teardown.
62
+ # @return [void]
44
63
  def reset!
45
64
  @mutex.synchronize { @registry.clear }
46
65
  end
@@ -8,14 +8,23 @@ module Seams
8
8
  # transport (ActiveSupport::Notifications, an external queue, etc.) and
9
9
  # implement the three primitives below.
10
10
  class Adapter
11
+ # Deliver an event (a name plus a Hash payload) to the transport.
12
+ # @return [void]
13
+ # @raise [NotImplementedError] unless a subclass overrides it.
11
14
  def publish(_event_name, _payload)
12
15
  raise NotImplementedError, "#{self.class} must implement #publish"
13
16
  end
14
17
 
18
+ # Register a block to run when the named event is published.
19
+ # @return [Object] a transport-specific subscriber handle.
20
+ # @raise [NotImplementedError] unless a subclass overrides it.
15
21
  def subscribe(_event_name, &)
16
22
  raise NotImplementedError, "#{self.class} must implement #subscribe"
17
23
  end
18
24
 
25
+ # Detach a previously-registered subscriber (the handle {#subscribe} returned).
26
+ # @return [void]
27
+ # @raise [NotImplementedError] unless a subclass overrides it.
19
28
  def unsubscribe(_subscriber)
20
29
  raise NotImplementedError, "#{self.class} must implement #unsubscribe"
21
30
  end
@@ -7,21 +7,25 @@ require "seams/events/adapter"
7
7
 
8
8
  module Seams
9
9
  module Events
10
+ # Namespace for the built-in event-bus adapters.
10
11
  module Adapters
11
12
  # Default in-process adapter, backed by ActiveSupport::Notifications.
12
13
  # Suitable for monolithic deployments — every engine in the host app
13
14
  # shares the same process, so subscribers fire synchronously after
14
15
  # the publisher's call returns.
15
16
  class ActiveSupport < Seams::Events::Adapter
17
+ # (see Seams::Events::Adapter#publish)
16
18
  def publish(event_name, payload)
17
19
  payload = { payload: payload } unless payload.is_a?(Hash)
18
20
  ::ActiveSupport::Notifications.instrument(event_name, payload)
19
21
  end
20
22
 
23
+ # (see Seams::Events::Adapter#subscribe)
21
24
  def subscribe(event_name, &)
22
25
  ::ActiveSupport::Notifications.subscribe(event_name, &)
23
26
  end
24
27
 
28
+ # (see Seams::Events::Adapter#unsubscribe)
25
29
  def unsubscribe(subscriber)
26
30
  ::ActiveSupport::Notifications.unsubscribe(subscriber)
27
31
  end
@@ -20,6 +20,17 @@ module Seams
20
20
  # convention that the boundary review catches.
21
21
  module Publisher
22
22
  class << self
23
+ # Publish a domain event to every subscriber.
24
+ #
25
+ # @param event_name [String, Symbol] the event name, in
26
+ # +resource.action.engine+ form (e.g. "subscription.created.billing").
27
+ # @param payload [Hash] data handed to each subscriber block.
28
+ # @raise [Seams::Events::InvalidEventNameError] if the name is malformed.
29
+ # @raise [Seams::Events::UnregisteredEventError] if no engine has
30
+ # registered the event via {Seams::EventRegistry.register}.
31
+ # @return [void]
32
+ # @example
33
+ # Seams::Events::Publisher.publish("invoice.paid.billing", invoice_id: 42)
23
34
  def publish(event_name, payload = {})
24
35
  Events.assert_valid_name!(event_name)
25
36
  name = event_name.to_s
@@ -34,6 +45,18 @@ module Seams
34
45
  adapter.publish(name, payload)
35
46
  end
36
47
 
48
+ # Subscribe a block to an event. Subscribers run **synchronously**
49
+ # in the publisher's thread — enqueue a background job for any
50
+ # network side effect rather than performing it inline. For a
51
+ # reload-safe, idempotent subscription prefer {attach_class}.
52
+ #
53
+ # @param event_name [String, Symbol] the event to listen for.
54
+ # @yieldparam payload [Hash] the published payload.
55
+ # @return [Object] an adapter-specific subscriber handle.
56
+ # @example
57
+ # Seams::Events::Publisher.subscribe("identity.signed_up.auth") do |payload|
58
+ # WelcomeJob.perform_later(payload[:identity_id])
59
+ # end
37
60
  def subscribe(event_name, &)
38
61
  Events.assert_valid_name!(event_name)
39
62
  name = event_name.to_s
@@ -115,6 +138,9 @@ module Seams
115
138
  end
116
139
  end
117
140
 
141
+ # Detach a subscriber handle from the adapter.
142
+ # @param subscriber [Object] the handle a subscribe call returned.
143
+ # @return [void]
118
144
  def unsubscribe(subscriber)
119
145
  adapter.unsubscribe(subscriber)
120
146
  end
@@ -143,6 +169,9 @@ module Seams
143
169
  subscriptions.reject { |name| EventRegistry.registered?(name) }.uniq
144
170
  end
145
171
 
172
+ # The configured event-bus adapter, instantiated lazily.
173
+ # @return [Seams::Events::Adapter] the adapter instance.
174
+ # @raise [Seams::ConfigurationError] if the configured class can't be loaded.
146
175
  def adapter
147
176
  @adapter ||= build_adapter
148
177
  end
data/lib/seams/events.rb CHANGED
@@ -9,6 +9,7 @@ module Seams
9
9
  # effects synchronously, so that the publisher's transaction can commit
10
10
  # quickly and side effects can retry independently.
11
11
  module Events
12
+ # Base class for event-bus errors.
12
13
  class Error < Seams::Error; end
13
14
 
14
15
  # Raised when an event name is published that no engine has registered.
@@ -59,6 +59,7 @@ module Seams
59
59
  write(File.join(engine_path, "spec/dummy/config/application.rb"), application_rb(engine_module))
60
60
  write(File.join(engine_path, "spec/dummy/config/environment.rb"), environment_rb)
61
61
  write(File.join(engine_path, "spec/dummy/config/database.yml"), database_yml(engine_path))
62
+ write(File.join(engine_path, "spec/dummy/config/cable.yml"), cable_yml)
62
63
  write(File.join(engine_path, "spec/dummy/config/environments/test.rb"), test_environment_rb)
63
64
  write(File.join(engine_path, "spec/dummy/config/initializers/secret_key.rb"), secret_key_rb)
64
65
  write(File.join(engine_path, "spec/dummy/config/routes.rb"), routes_rb(engine_module, mount_at))
@@ -163,6 +164,16 @@ module Seams
163
164
  YML
164
165
  end
165
166
 
167
+ # solid_cable >= 4.1 reads config/cable.yml eagerly at boot, so the
168
+ # dummy app must ship one even though engine specs never open a
169
+ # socket. The in-process test adapter needs no database or server.
170
+ def cable_yml
171
+ <<~YML
172
+ test:
173
+ adapter: test
174
+ YML
175
+ end
176
+
166
177
  def test_environment_rb
167
178
  <<~RB
168
179
  # frozen_string_literal: true
@@ -251,10 +262,20 @@ module Seams
251
262
  # inherit from ::ApplicationController (the seams:engine generator
252
263
  # default) can boot inside a request spec. Real hosts will have
253
264
  # their own; this is dummy-only.
265
+ #
266
+ # `authenticate_identity!` is stubbed as a no-op so engine request
267
+ # specs pass by default (simulating an authenticated session).
268
+ # Specs that need to verify the authentication gate should stub or
269
+ # override this method, or use the `sign_in_as` helper when present.
254
270
  <<~RB
255
271
  # frozen_string_literal: true
256
272
 
257
273
  class ApplicationController < ActionController::Base
274
+ def authenticate_identity!
275
+ # Stub: always passes in the dummy app. Engine specs that want to
276
+ # exercise the unauthenticated path should stub this method or
277
+ # call `sign_in_as` from the authentication test helper.
278
+ end
258
279
  end
259
280
  RB
260
281
  end
@@ -67,7 +67,7 @@ module Seams
67
67
  # assert_marker_exists!). Splitting them into separate blocks
68
68
  # would obscure the "these are the public follow-up generator
69
69
  # primitives" grouping. Disable BlockLength for this one block.
70
- # rubocop:disable Metrics/BlockLength
70
+ # rubocop:disable-next Metrics/BlockLength
71
71
  no_tasks do
72
72
  # Resolve a path inside the host's engines/<this_engine>/.
73
73
  # Mirrors the canonical generators' +engine_path+ method but
@@ -130,7 +130,6 @@ module Seams
130
130
  MSG
131
131
  end
132
132
  end
133
- # rubocop:enable Metrics/BlockLength
134
133
 
135
134
  # Subclass override-point. Subclasses define their own public
136
135
  # method named `report_summary` (or any other end-of-run name);
@@ -114,9 +114,16 @@ module Seams
114
114
  end
115
115
 
116
116
  def build_gem_line(name, args, group)
117
- version_part = args.first.is_a?(String) ? %(, "#{args.first}") : ""
117
+ # Every String arg is a version requirement, so a range such as
118
+ # (">= 7.1", "< 9") is emitted in full.
119
+ version_part = args.grep(String).map { |req| %(, "#{req}") }.join
118
120
  gem_line = %(gem "#{name}"#{version_part})
119
- gem_line = "group #{group.inspect} do\n #{gem_line}\nend" if group
121
+ # group may be a single symbol (:development) or several
122
+ # (%i[development test] -> `group :development, :test do`).
123
+ if group
124
+ groups = Array(group).map(&:inspect).join(", ")
125
+ gem_line = "group #{groups} do\n #{gem_line}\nend"
126
+ end
120
127
  gem_line
121
128
  end
122
129
  end
@@ -11,8 +11,8 @@ module Seams
11
11
  # the whole file. The Splicer is the shared primitive: every
12
12
  # follow-up generator funnels through these methods.
13
13
  #
14
- # See doc/INSERTION_POINTS.md for the marker format spec and
15
- # doc/INSERTION_POINTS_CATALOGUE.md for the canonical list of
14
+ # See doc/reference/INSERTION_POINTS.md for the marker format spec and
15
+ # doc/reference/INSERTION_POINTS_CATALOGUE.md for the canonical list of
16
16
  # markers each engine ships.
17
17
  #
18
18
  # Example:
@@ -9,22 +9,44 @@ module Seams
9
9
  # Datadog, OpenTelemetry, etc.) — engines never speak to those
10
10
  # libraries directly.
11
11
  class Adapter
12
+ # Log a debug-level message with structured context.
13
+ # @param _message [String] the log message.
14
+ # @param _context [Hash] structured key/value context (engine, actor id, ...).
15
+ # @return [void]
16
+ # @raise [NotImplementedError] unless a subclass overrides it.
12
17
  def debug(_message, **_context)
13
18
  raise NotImplementedError, "#{self.class} must implement #debug"
14
19
  end
15
20
 
21
+ # Log an info-level message with structured context.
22
+ # @param (see #debug)
23
+ # @return [void]
24
+ # @raise [NotImplementedError] unless a subclass overrides it.
16
25
  def info(_message, **_context)
17
26
  raise NotImplementedError, "#{self.class} must implement #info"
18
27
  end
19
28
 
29
+ # Log a warn-level message with structured context.
30
+ # @param (see #debug)
31
+ # @return [void]
32
+ # @raise [NotImplementedError] unless a subclass overrides it.
20
33
  def warn(_message, **_context)
21
34
  raise NotImplementedError, "#{self.class} must implement #warn"
22
35
  end
23
36
 
37
+ # Log an error-level message with structured context.
38
+ # @param (see #debug)
39
+ # @return [void]
40
+ # @raise [NotImplementedError] unless a subclass overrides it.
24
41
  def error(_message, **_context)
25
42
  raise NotImplementedError, "#{self.class} must implement #error"
26
43
  end
27
44
 
45
+ # Time the given block and emit the duration as structured context.
46
+ # @param _operation [String] a label for the timed operation.
47
+ # @param _context [Hash] structured key/value context.
48
+ # @return [Object] whatever the block returns.
49
+ # @raise [NotImplementedError] unless a subclass overrides it.
28
50
  def measure(_operation, **_context, &)
29
51
  raise NotImplementedError, "#{self.class} must implement #measure"
30
52
  end
@@ -9,13 +9,19 @@ module Seams
9
9
  # logs can be parsed and filtered uniformly by the host application's
10
10
  # log aggregator.
11
11
  module Observability
12
+ # Base class for observability errors.
12
13
  class Error < Seams::Error; end
13
14
 
14
15
  class << self
16
+ # The configured observability adapter, instantiated lazily.
17
+ # @return [Seams::Observability::Adapter] the adapter instance.
18
+ # @raise [Seams::ConfigurationError] if the configured class can't be loaded.
15
19
  def adapter
16
20
  @adapter ||= build_adapter
17
21
  end
18
22
 
23
+ # Drop the memoized adapter so the next call rebuilds it. Test hook.
24
+ # @return [void]
19
25
  def reset!
20
26
  @adapter = nil
21
27
  end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "seams/permissions"
4
+
5
+ module Seams
6
+ # Tracks every ability code that an engine has declared it understands,
7
+ # along with the engine that owns it. Acts as the single source of truth
8
+ # for authorisation lookups and prevents two engines from claiming the
9
+ # same ability code.
10
+ module PermissionRegistry
11
+ @registry = {}
12
+ @mutex = Mutex.new
13
+
14
+ class << self
15
+ # Declare that an engine understands an ability code. Call this from
16
+ # the engine that owns the ability so authorisation lookups have a
17
+ # single source of truth.
18
+ #
19
+ # @param ability [String, Symbol] the ability code (e.g. "billing.manage").
20
+ # @param owned_by [String] the owning engine (e.g. "Billing").
21
+ # @raise [Seams::Permissions::DuplicateAbilityError] if another engine owns it.
22
+ # @return [String] the owning engine name.
23
+ # @example
24
+ # Seams::PermissionRegistry.register("billing.manage", owned_by: "Billing")
25
+ def register(ability, owned_by:)
26
+ Permissions.assert_valid_name!(ability)
27
+ ability = ability.to_s
28
+
29
+ @mutex.synchronize do
30
+ existing = @registry[ability]
31
+
32
+ if existing && existing != owned_by
33
+ raise Permissions::DuplicateAbilityError,
34
+ "Ability #{ability.inspect} already registered by #{existing.inspect}; " \
35
+ "cannot also be registered by #{owned_by.inspect}"
36
+ end
37
+
38
+ @registry[ability] = owned_by
39
+ end
40
+ end
41
+
42
+ # @param ability [String, Symbol] an ability code.
43
+ # @return [Boolean] whether any engine has registered it.
44
+ def registered?(ability)
45
+ @registry.key?(ability.to_s)
46
+ end
47
+
48
+ # @param ability [String, Symbol] an ability code.
49
+ # @return [String, nil] the owning engine, or nil if unregistered.
50
+ def owner_of(ability)
51
+ @registry[ability.to_s]
52
+ end
53
+
54
+ # @return [Hash{String=>String}] a copy of the full ability => owner map.
55
+ def all
56
+ @registry.dup
57
+ end
58
+
59
+ # Clear the registry. Intended for test teardown.
60
+ # @return [void]
61
+ def reset!
62
+ @mutex.synchronize { @registry.clear }
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Seams
4
+ # Permissions module — public API for authorisation across engines.
5
+ #
6
+ # Engines register the ability codes they understand at boot through
7
+ # Seams::PermissionRegistry, mirroring the event bus. Authorisation is
8
+ # resolved through Seams::Permissions.can?, which answers from a
9
+ # code-defined role hierarchy and a host-configurable grant map. Nothing
10
+ # here enforces permissions on a request; it is the machinery that the
11
+ # request and controller layers build on in later phases.
12
+ module Permissions
13
+ class Error < Seams::Error; end
14
+
15
+ # Raised when a permission is checked for an ability that no engine has
16
+ # registered.
17
+ class UnregisteredAbilityError < Error; end
18
+
19
+ # Raised when two engines try to register the same ability code.
20
+ class DuplicateAbilityError < Error; end
21
+
22
+ # Raised when an ability code doesn't follow the resource.action.engine
23
+ # convention (e.g., "invoice.read.billing").
24
+ class InvalidAbilityNameError < Error; end
25
+
26
+ # Three dot-separated segments, lowercase, snake_case allowed. Matches
27
+ # the event name convention so abilities and events read alike.
28
+ NAME_PATTERN = /\A[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*\z/
29
+
30
+ # Roles ordered from most to least privileged. Each role inherits every
31
+ # ability granted to the roles below it (owner ⊇ admin ⊇ member).
32
+ ROLE_HIERARCHY = %w[owner admin member].freeze
33
+
34
+ # A pseudo-role that bypasses every check — used by trusted internal
35
+ # callers such as background jobs and system actors.
36
+ SYSTEM_ROLE = "system"
37
+
38
+ # Sensible default grant map so `can?` allows things out of the box,
39
+ # keyed by role. `member` may read; `admin` may manage. `owner`
40
+ # inherits everything granted to `admin` (and through it `member`)
41
+ # via ROLE_HIERARCHY, and `system` bypasses checks entirely — so
42
+ # neither needs its own entry here. Hosts override the whole map
43
+ # through `Seams.configuration.permission_grants`. The codes mirror
44
+ # what each canonical engine registers from its engine.rb.
45
+ DEFAULT_GRANTS = {
46
+ "member" => %w[
47
+ identity.read.auth
48
+ invoice.read.billing
49
+ subscription.read.billing
50
+ team.read.teams
51
+ account.read.accounts
52
+ membership.read.accounts
53
+ notification.read.notifications
54
+ ].freeze,
55
+ "admin" => %w[
56
+ identity.manage.auth
57
+ invoice.manage.billing
58
+ subscription.manage.billing
59
+ plan.manage.billing
60
+ lifetime.manage.billing
61
+ team.manage.teams
62
+ member.manage.teams
63
+ invitation.manage.teams
64
+ account.manage.accounts
65
+ membership.manage.accounts
66
+ notification.manage.notifications
67
+ preference.manage.notifications
68
+ ].freeze
69
+ }.freeze
70
+
71
+ def self.valid_name?(name)
72
+ NAME_PATTERN.match?(name.to_s)
73
+ end
74
+
75
+ def self.assert_valid_name!(name)
76
+ return if valid_name?(name)
77
+
78
+ raise InvalidAbilityNameError,
79
+ "Ability name #{name.inspect} must follow resource.action.engine " \
80
+ "(e.g. invoice.read.billing)"
81
+ end
82
+
83
+ # Resolves whether a role is allowed an ability. The role string is one
84
+ # of owner/admin/member/system; request-layer bypasses (e.g. staff?)
85
+ # are handled by callers, not here. Raises if the ability was never
86
+ # registered, matching the event bus's loud-failure behaviour.
87
+ def self.can?(role:, ability:)
88
+ ability = ability.to_s
89
+
90
+ unless PermissionRegistry.registered?(ability)
91
+ raise UnregisteredAbilityError,
92
+ "Ability #{ability.inspect} is not registered; register it with " \
93
+ "Seams::PermissionRegistry.register before checking it"
94
+ end
95
+
96
+ role = role.to_s
97
+ return true if role == SYSTEM_ROLE
98
+
99
+ roles_for(role).any? { |r| granted_abilities_for(r).include?(ability) }
100
+ end
101
+
102
+ # The role itself plus every role it inherits from, lowest first. An
103
+ # unknown role resolves to just itself (and therefore no grants).
104
+ def self.roles_for(role)
105
+ role = role.to_s
106
+ index = ROLE_HIERARCHY.index(role)
107
+ return [role] unless index
108
+
109
+ ROLE_HIERARCHY[index..]
110
+ end
111
+
112
+ # Abilities granted directly to a single role in the configured map,
113
+ # tolerating string or symbol keys and values.
114
+ def self.granted_abilities_for(role)
115
+ grants = Seams.configuration.permission_grants
116
+ list = grants[role.to_s] || grants[role.to_sym]
117
+ Array(list).map(&:to_s)
118
+ end
119
+ end
120
+ end
data/lib/seams/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Seams
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/seams.rb CHANGED
@@ -6,8 +6,13 @@ require "seams/version"
6
6
  #
7
7
  # See https://github.com/Davidslv/seams for documentation.
8
8
  module Seams
9
+ # Base class for all Seams errors.
9
10
  class Error < StandardError; end
11
+
12
+ # Raised when Seams is misconfigured (e.g. an adapter class can't be loaded).
10
13
  class ConfigurationError < Error; end
14
+
15
+ # Raised when a generator hits an unrecoverable problem.
11
16
  class GeneratorError < Error; end
12
17
  end
13
18
 
@@ -17,6 +22,8 @@ require "seams/events"
17
22
  require "seams/events/adapter"
18
23
  require "seams/events/adapters/active_support"
19
24
  require "seams/event_registry"
25
+ require "seams/permissions"
26
+ require "seams/permission_registry"
20
27
  require "seams/events/publisher"
21
28
  require "seams/observability"
22
29
  require "seams/observability/adapter"