parameters:
  paths:
    - ./modules
    - ./app

  # Every module gets two layers: its Public surface (the only classes other
  # modules may depend on) and its Internal implementation (repositories,
  # private models, helpers). This is the PHP-side enforcement of the same
  # rule the original TS codebase's dependency-cruiser config enforced:
  # "no module may import another module's internal/ directory, and no
  # module may import anything but another module's public interface."
  layers:
    # Foundational contracts/value-objects every module is allowed to
    # depend on (App\Contracts\DomainEvent, App\Support\Result) — kept
    # separate from the "App" layer below, which is app-orchestration code
    # that depends ON modules, not the other way around.
    - name: SharedKernel
      collectors:
        - type: classNameRegex
          value: '#^App\\(Contracts|Support)\\.*#'

    - name: App
      collectors:
        - type: classNameRegex
          value: '#^App\\(?!Contracts\\|Support\\).*#'

    # --- Leaf modules (Phase: Port leaf modules) ---
    - name: SettingsPublic
      collectors: [{ type: classNameRegex, value: '#^Modules\\Settings\\(?!Internal\\).*#' }]
    - name: SettingsInternal
      collectors: [{ type: classNameRegex, value: '#^Modules\\Settings\\Internal\\.*#' }]

    - name: UsersPublic
      collectors: [{ type: classNameRegex, value: '#^Modules\\Users\\(?!Internal\\).*#' }]
    - name: UsersInternal
      collectors: [{ type: classNameRegex, value: '#^Modules\\Users\\Internal\\.*#' }]

    - name: MediaPublic
      collectors: [{ type: classNameRegex, value: '#^Modules\\Media\\(?!Internal\\).*#' }]
    - name: MediaInternal
      collectors: [{ type: classNameRegex, value: '#^Modules\\Media\\Internal\\.*#' }]

    - name: TaxPublic
      collectors: [{ type: classNameRegex, value: '#^Modules\\Tax\\(?!Internal\\).*#' }]
    - name: TaxInternal
      collectors: [{ type: classNameRegex, value: '#^Modules\\Tax\\Internal\\.*#' }]

    - name: ShippingPublic
      collectors: [{ type: classNameRegex, value: '#^Modules\\Shipping\\(?!Internal\\).*#' }]
    - name: ShippingInternal
      collectors: [{ type: classNameRegex, value: '#^Modules\\Shipping\\Internal\\.*#' }]

    - name: DiscountsPublic
      collectors: [{ type: classNameRegex, value: '#^Modules\\Discounts\\(?!Internal\\).*#' }]
    - name: DiscountsInternal
      collectors: [{ type: classNameRegex, value: '#^Modules\\Discounts\\Internal\\.*#' }]

    - name: GiftCardsPublic
      collectors: [{ type: classNameRegex, value: '#^Modules\\GiftCards\\(?!Internal\\).*#' }]
    - name: GiftCardsInternal
      collectors: [{ type: classNameRegex, value: '#^Modules\\GiftCards\\Internal\\.*#' }]

    - name: InventoryPublic
      collectors: [{ type: classNameRegex, value: '#^Modules\\Inventory\\(?!Internal\\).*#' }]
    - name: InventoryInternal
      collectors: [{ type: classNameRegex, value: '#^Modules\\Inventory\\Internal\\.*#' }]

    - name: AnalyticsPublic
      collectors: [{ type: classNameRegex, value: '#^Modules\\Analytics\\(?!Internal\\).*#' }]
    - name: AnalyticsInternal
      collectors: [{ type: classNameRegex, value: '#^Modules\\Analytics\\Internal\\.*#' }]

    # License validation against evodesignco.com's shared license server —
    # a leaf module like Settings, depends only on SettingsPublic (state is
    # stored as an opaque JSON blob under a Settings key).
    - name: LicensePublic
      collectors: [{ type: classNameRegex, value: '#^Modules\\License\\(?!Internal\\).*#' }]
    - name: LicenseInternal
      collectors: [{ type: classNameRegex, value: '#^Modules\\License\\Internal\\.*#' }]

    # --- Core commerce modules ---
    - name: CatalogPublic
      collectors: [{ type: classNameRegex, value: '#^Modules\\Catalog\\(?!Internal\\).*#' }]
    - name: CatalogInternal
      collectors: [{ type: classNameRegex, value: '#^Modules\\Catalog\\Internal\\.*#' }]

    - name: NotificationsPublic
      collectors: [{ type: classNameRegex, value: '#^Modules\\Notifications\\(?!Internal\\).*#' }]
    - name: NotificationsInternal
      collectors: [{ type: classNameRegex, value: '#^Modules\\Notifications\\Internal\\.*#' }]

    - name: SearchPublic
      collectors: [{ type: classNameRegex, value: '#^Modules\\Search\\(?!Internal\\).*#' }]
    - name: SearchInternal
      collectors: [{ type: classNameRegex, value: '#^Modules\\Search\\Internal\\.*#' }]

    - name: CustomersPublic
      collectors: [{ type: classNameRegex, value: '#^Modules\\Customers\\(?!Internal\\).*#' }]
    - name: CustomersInternal
      collectors: [{ type: classNameRegex, value: '#^Modules\\Customers\\Internal\\.*#' }]

    - name: PaymentsPublic
      collectors: [{ type: classNameRegex, value: '#^Modules\\Payments\\(?!Internal\\).*#' }]
    - name: PaymentsInternal
      collectors: [{ type: classNameRegex, value: '#^Modules\\Payments\\Internal\\.*#' }]

    - name: OrdersPublic
      collectors: [{ type: classNameRegex, value: '#^Modules\\Orders\\(?!Internal\\).*#' }]
    - name: OrdersInternal
      collectors: [{ type: classNameRegex, value: '#^Modules\\Orders\\Internal\\.*#' }]

    # --- Intelligence modules ---
    - name: MarketingPublic
      collectors: [{ type: classNameRegex, value: '#^Modules\\Marketing\\(?!Internal\\).*#' }]
    - name: MarketingInternal
      collectors: [{ type: classNameRegex, value: '#^Modules\\Marketing\\Internal\\.*#' }]

    - name: AutomationPublic
      collectors: [{ type: classNameRegex, value: '#^Modules\\Automation\\(?!Internal\\).*#' }]
    - name: AutomationInternal
      collectors: [{ type: classNameRegex, value: '#^Modules\\Automation\\Internal\\.*#' }]

    - name: MerchantHealthPublic
      collectors: [{ type: classNameRegex, value: '#^Modules\\MerchantHealth\\(?!Internal\\).*#' }]
    - name: MerchantHealthInternal
      collectors: [{ type: classNameRegex, value: '#^Modules\\MerchantHealth\\Internal\\.*#' }]

    # --- Growth automations: new infra, not ported from TS ---
    - name: WishlistPublic
      collectors: [{ type: classNameRegex, value: '#^Modules\\Wishlist\\(?!Internal\\).*#' }]
    - name: WishlistInternal
      collectors: [{ type: classNameRegex, value: '#^Modules\\Wishlist\\Internal\\.*#' }]

    - name: CartRecoveryPublic
      collectors: [{ type: classNameRegex, value: '#^Modules\\CartRecovery\\(?!Internal\\).*#' }]
    - name: CartRecoveryInternal
      collectors: [{ type: classNameRegex, value: '#^Modules\\CartRecovery\\Internal\\.*#' }]

    - name: ReviewsPublic
      collectors: [{ type: classNameRegex, value: '#^Modules\\Reviews\\(?!Internal\\).*#' }]
    - name: ReviewsInternal
      collectors: [{ type: classNameRegex, value: '#^Modules\\Reviews\\Internal\\.*#' }]

    - name: StorefrontLayoutsPublic
      collectors: [{ type: classNameRegex, value: '#^Modules\\StorefrontLayouts\\(?!Internal\\).*#' }]
    - name: StorefrontLayoutsInternal
      collectors: [{ type: classNameRegex, value: '#^Modules\\StorefrontLayouts\\Internal\\.*#' }]

    - name: BrandingPublic
      collectors: [{ type: classNameRegex, value: '#^Modules\\Branding\\(?!Internal\\).*#' }]
    - name: BrandingInternal
      collectors: [{ type: classNameRegex, value: '#^Modules\\Branding\\Internal\\.*#' }]

    # --- Orchestrator (owns no schema) ---
    - name: CommerceCorePublic
      collectors: [{ type: classNameRegex, value: '#^Modules\\CommerceCore\\(?!Internal\\).*#' }]
    - name: CommerceCoreInternal
      collectors: [{ type: classNameRegex, value: '#^Modules\\CommerceCore\\Internal\\.*#' }]

  # Ruleset: mirrors the dependency graph from the migration plan (§2).
  # A module's *Internal layer may only be depended on by itself (implicit —
  # deptrac only flags layers listed as "not ruleset", so simply never
  # granting another layer access to an Internal layer is what enforces
  # this). Each Public layer below lists exactly which other Public layers
  # it is allowed to depend on, matching public.ts import graphs 1:1.
  ruleset:
    App:
      - SharedKernel
      - SettingsPublic
      - UsersPublic
      - MediaPublic
      - TaxPublic
      - ShippingPublic
      - DiscountsPublic
      - InventoryPublic
      - AnalyticsPublic
      - LicensePublic
      - CatalogPublic
      - NotificationsPublic
      - SearchPublic
      - CustomersPublic
      - PaymentsPublic
      - OrdersPublic
      - MarketingPublic
      - AutomationPublic
      - MerchantHealthPublic
      - WishlistPublic
      - CartRecoveryPublic
      - CommerceCorePublic
      - ReviewsPublic
      - GiftCardsPublic

    # Every module's Public layer wraps its own Internal (repositories/
    # models) — Public depends on Internal, never the reverse. Every
    # layer may also use the shared DomainEvent interface and Result value
    # object (see App\Contracts, App\Support).
    # UsersPublic added for FeatureFlagController's Recipient system — it
    # resolves an internal automation's selected role ids to real role
    # names via UsersService::listRoles().
    SettingsPublic: [SettingsInternal, UsersPublic, SharedKernel]
    SettingsInternal: [SharedKernel]
    # NotificationsPublic added for the "New Staff Member" Notification
    # Centre listener in createStaffUser() — a real cycle with
    # NotificationsPublic (which already constructor-injects UsersService),
    # resolved the same lazy-container-resolution way as every other cycle
    # in this graph: the listener calls app(NotificationsService::class)
    # rather than constructor-injecting it.
    UsersPublic: [UsersInternal, NotificationsPublic, SharedKernel]
    UsersInternal: [SharedKernel]
    MediaPublic: [MediaInternal, SharedKernel]
    MediaInternal: [SharedKernel]
    TaxPublic: [TaxInternal, SharedKernel]
    TaxInternal: [SharedKernel]
    ShippingPublic: [ShippingInternal, SharedKernel]
    ShippingInternal: [SharedKernel]
    DiscountsPublic: [DiscountsInternal, SharedKernel]
    DiscountsInternal: [SharedKernel]
    GiftCardsPublic: [GiftCardsInternal, SharedKernel]
    GiftCardsInternal: [SharedKernel]
    # SettingsPublic added for InventoryService's default reorder-point
    # lookup (Automation Centre's Low Stock Alert store-wide default),
    # same precedent as NotificationsPublic below.
    InventoryPublic: [InventoryInternal, SettingsPublic, SharedKernel]
    InventoryInternal: [SharedKernel]

    # CatalogPublic added for Listeners\RecordProductView, which imports
    # Modules\Catalog\Events\ProductViewed (Catalog's Public layer) — the
    # listener itself lives in Analytics' Public layer, same as every other
    # module's Listeners/ directory. CustomersPublic added for
    # AnalyticsService's new_customer_count special case (getMetricSeries())
    # — a declared cycle with CustomersPublic's own existing dependency on
    # AnalyticsPublic (for its Listeners\RecordCustomerRegisteredAnalytics),
    # but not a real one: neither module's constructor-injects the other
    # (CustomersService doesn't need AnalyticsService; the listener that
    # does is a separate class), so the container never has to build one
    # while building the other.
    AnalyticsPublic: [AnalyticsInternal, CatalogPublic, CustomersPublic, SharedKernel]
    AnalyticsInternal: [CatalogPublic, SharedKernel]

    LicensePublic: [LicenseInternal, SettingsPublic, SharedKernel]
    LicenseInternal: [SharedKernel]

    # InventoryPublic is here only for StorefrontProductController's route-
    # layer composition (variants + live stock) — CatalogService itself
    # never calls Inventory, same "compose at the app layer, not module-to-
    # module" discipline as the old recommendation-to-campaign bridge.
    CatalogPublic: [CatalogInternal, MediaPublic, InventoryPublic, SharedKernel]
    CatalogInternal: [MediaPublic, SharedKernel]

    # OrdersPublic added for listAutomationLog()'s orderNumber resolution —
    # Orders already constructor-injects NotificationsService, so this is a
    # genuine cycle resolved the same lazy-container-resolution way as the
    # pre-existing Customers/Orders/Notifications cycle (see this class's
    # own doc comment).
    NotificationsPublic: [NotificationsInternal, CustomersPublic, UsersPublic, SettingsPublic, OrdersPublic, SharedKernel]
    NotificationsInternal: [CustomersPublic, UsersPublic, SettingsPublic, OrdersPublic, SharedKernel]

    SearchPublic: [SearchInternal, CatalogPublic, SharedKernel]
    SearchInternal: [CatalogPublic, SharedKernel]

    # AnalyticsPublic added to these three only for the RecordXAnalytics
    # listeners (Orders/Payments/Customers -> AnalyticsService::recordEvent())
    # that feed the Reports page's metrics — Analytics itself still has zero
    # outbound dependencies, so this can never become a cycle.
    # NotificationsPublic added for registerCustomer()'s email-verification
    # send — a real cycle with NotificationsPublic (which already depends
    # on CustomersPublic), safe because NotificationsService resolves
    # CustomersService lazily (via the container) rather than constructor-
    # injecting it, so constructing one side never eagerly needs the other.
    # UsersPublic added for the "New Customer" Notification Centre listener
    # (CustomerRegistered -> role-resolved staff notification), same
    # UsersService::listStaffUsersByRoleIds() pattern as Automation/
    # MerchantHealth's own Recipient system.
    CustomersPublic: [CustomersInternal, OrdersPublic, AnalyticsPublic, NotificationsPublic, SettingsPublic, UsersPublic, SharedKernel]
    CustomersInternal: [OrdersPublic, NotificationsPublic, UsersPublic, SharedKernel]

    # NotificationsPublic + UsersPublic added for the "Payment Failed"
    # Notification Centre listener at authorizeAndCapture()'s existing
    # DECLINED path — role-resolved staff notification, same pattern as
    # every other Recipient-system consumer.
    PaymentsPublic: [PaymentsInternal, SettingsPublic, AnalyticsPublic, NotificationsPublic, UsersPublic, SharedKernel]
    PaymentsInternal: [SettingsPublic, NotificationsPublic, UsersPublic, SharedKernel]

    # SettingsPublic added for the per-workflow enable/disable feature-flag
    # guard each of the 6 lifecycle email methods now checks before sending.
    # InventoryPublic added for refundOrder()'s restock call. CatalogPublic
    # added so the order-confirmation email can show each line item's real
    # product photo (Catalog has no reverse dependency on Orders, so this
    # stays acyclic). UsersPublic added for the new staff-facing Order/
    # Payment/Refund Notification Centre listeners' role resolution.
    OrdersPublic: [OrdersInternal, CustomersPublic, NotificationsPublic, PaymentsPublic, ShippingPublic, AnalyticsPublic, SettingsPublic, InventoryPublic, CatalogPublic, UsersPublic, SharedKernel]
    OrdersInternal: [CustomersPublic, NotificationsPublic, PaymentsPublic, ShippingPublic, SettingsPublic, InventoryPublic, UsersPublic, SharedKernel]

    MarketingPublic: [MarketingInternal, CustomersPublic, NotificationsPublic, MediaPublic, SharedKernel]
    MarketingInternal: [CustomersPublic, NotificationsPublic, SharedKernel]

    # SettingsPublic added for handleStockLowEvent()'s feature-flag guard.
    # AnalyticsPublic added for SlowMovingInventoryRecommender's view-count
    # enrichment and the new SendSlowMovingInventoryAlert job.
    # UsersPublic added for the Recipient system — AutomationService::
    # handleStockLowEvent() and SendSlowMovingInventoryAlert resolve a
    # selected role set to real staff accounts via
    # UsersService::listStaffUsersByRoleIds() instead of hardcoding 'owner'.
    AutomationPublic: [AutomationInternal, InventoryPublic, NotificationsPublic, CatalogPublic, OrdersPublic, CustomersPublic, SettingsPublic, AnalyticsPublic, UsersPublic, SharedKernel]
    AutomationInternal: [InventoryPublic, NotificationsPublic, CatalogPublic, OrdersPublic, CustomersPublic, SettingsPublic, AnalyticsPublic, UsersPublic, SharedKernel]

    # NotificationsPublic added for the Weekly Merchant Summary automation
    # (Jobs\SendWeeklyMerchantSummary sends the digest email). CartRecoveryPublic/
    # CustomersPublic/CatalogPublic added for buildWeeklyDigest()'s expanded
    # sections (abandoned carts, new/returning customers, best sellers/top
    # categories) — same "widen this module's ruleset when the weekly
    # summary needs a new read model" precedent as NotificationsPublic.
    # UsersPublic added for the Recipient system — SendWeeklyMerchantSummary
    # resolves a selected role set to real staff accounts via
    # UsersService::listStaffUsersByRoleIds() instead of hardcoding 'owner'.
    MerchantHealthPublic: [MerchantHealthInternal, AnalyticsPublic, AutomationPublic, OrdersPublic, NotificationsPublic, CartRecoveryPublic, CustomersPublic, CatalogPublic, SettingsPublic, UsersPublic, SharedKernel]
    MerchantHealthInternal: [AnalyticsPublic, AutomationPublic, OrdersPublic, NotificationsPublic, CartRecoveryPublic, CustomersPublic, CatalogPublic, SettingsPublic, UsersPublic, SharedKernel]

    # Backs Wishlist Back-in-Stock + Back-in-Stock Notification (new infra,
    # not a port) — CustomersPublic to resolve a guest email into a
    # customer id (same guest-checkout pattern), NotificationsPublic to
    # send, CatalogPublic for the product name in the email, InventoryPublic
    # only for the Events\StockBackInStock class the listener imports.
    # SettingsPublic added for notifySubscribersForVariant()'s feature-flag guard.
    WishlistPublic: [WishlistInternal, CustomersPublic, NotificationsPublic, CatalogPublic, InventoryPublic, SettingsPublic, SharedKernel]
    WishlistInternal: [CustomersPublic, NotificationsPublic, CatalogPublic, InventoryPublic, SettingsPublic, SharedKernel]

    # Abandoned Cart Recovery — new infra, not a port. NotificationsPublic
    # to send the recovery email, CatalogPublic for product names in it,
    # CustomersPublic to resolve the captured email into a customer id
    # (same guest-checkout / Wishlist pattern) since a cart abandoner may
    # never have completed findOrCreateCustomer(). No OrdersPublic
    # dependency — "recovered" is decided by Commerce Core calling
    # markRecovered() when checkout() succeeds, not by this module
    # querying Orders itself.
    # SettingsPublic added for the two-tier reminder job's per-tier
    # feature-flag guard.
    CartRecoveryPublic: [CartRecoveryInternal, NotificationsPublic, CatalogPublic, CustomersPublic, SettingsPublic, SharedKernel]
    CartRecoveryInternal: [NotificationsPublic, CatalogPublic, CustomersPublic, SettingsPublic, SharedKernel]

    # Needs Catalog to validate a product exists and to resolve product
    # names for the admin moderation queue; Customers to resolve a
    # reviewer's name/email for that same queue.
    ReviewsPublic: [ReviewsInternal, CatalogPublic, CustomersPublic, SharedKernel]
    ReviewsInternal: [CatalogPublic, CustomersPublic, SharedKernel]

    # MediaPublic resolves a `media`-kind region's content_source_ref (an
    # uploaded asset id) into a real image URL. Every other content-source
    # kind still only records its kind — nothing resolves those yet.
    StorefrontLayoutsPublic: [StorefrontLayoutsInternal, MediaPublic, SettingsPublic, SharedKernel]
    StorefrontLayoutsInternal: [SharedKernel]

    # MediaPublic isn't used for asset resolution here (Branding stores
    # deterministic-path logo/favicon URLs directly, not Asset/AssetVariant
    # records — see BrandingService's doc comment) but the module still
    # goes through Storage::disk('media'), the same disk Media itself
    # owns, so a future S3/R2 swap only ever touches one config point.
    # SettingsPublic is the actual persistence — one JSON blob, same
    # posture as StorefrontLayoutsPublic/LicensePublic above.
    BrandingPublic: [BrandingInternal, SettingsPublic, SharedKernel]
    BrandingInternal: [SharedKernel]

    CommerceCorePublic:
      - CommerceCoreInternal
      - CartRecoveryPublic
      - CatalogPublic
      - CustomersPublic
      - DiscountsPublic
      - GiftCardsPublic
      - InventoryPublic
      - OrdersPublic
      - PaymentsPublic
      - SettingsPublic
      - ShippingPublic
      - TaxPublic
      - UsersPublic
      - SharedKernel
    CommerceCoreInternal:
      - CartRecoveryPublic
      - CatalogPublic
      - CustomersPublic
      - DiscountsPublic
      - InventoryPublic
      - OrdersPublic
      - PaymentsPublic
      - SettingsPublic
      - ShippingPublic
      - TaxPublic
      - UsersPublic
      - SharedKernel
