# Temporary migration inventory. See README.md before updating.
# Entry points are relative to this directory; evidence paths are relative to ghost.
# An unverified facet is an open audit question, not an empty/no-op contract.
exemplaryMigration: indexnow-ping
services:
  activitypub:
    kind: directory
    disposition: migrate
    status: audited
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: complex
      modelImpact: fits
      reason: >-
        The retained instance fits the helper, but startup currently publishes it before
        registering three settings listeners and launches enable/disable without awaiting
        it. Define readiness as composition versus remote configuration explicitly, preserve
        background boot, and add cleanup plus ordering for overlapping settings changes.
        Dynamic enable/disable can remain service behavior rather than requiring instance
        replacement.
      reviewedAt: f276513ba32b7114fd5cc4f7b02d3f82c0c31e3e
      sources:
        - core/server/services/activitypub/activity-pub-service-wrapper.js
        - core/server/services/activitypub/activity-pub-service.ts
        - core/boot.js
        - core/server/services/settings/settings-service.js
      dependencies:
        complexity: straightforward
        reason: >-
          The implementation already receives knex, site URL, logging and Identity Tokens; only the
          separate wrapper owns settings and event wiring. Moving that composition to index.ts and
          making the IdentityTokenService import type-only is bounded. The asynchronous settings
          reconciliation and subscription cleanup remain lifecycle concerns, not evidence of a
          dependency cycle.
        preparation:
          - >-
            Move wrapper composition into the root and retain signing-keys then Identity Tokens then
            ActivityPub readiness; update the instance consumer without resolving Identity Tokens
            earlier.
          - >-
            Keep settings reads live and pass or retain the event binding at the root; characterize
            background configuration separately from construction.
        reviewedAt: 44ad405bff4a8700af2ed51f833ebc08d25dddff
        sources:
          - core/server/services/activitypub/activity-pub-service-wrapper.js
          - core/server/services/activitypub/activity-pub-service.ts
          - core/server/services/identity-tokens/identity-token-service-wrapper.js
          - core/boot.js
    entryPoints:
      - activitypub/index.js
    notes: >-
      Preserve background startup and settings-driven enable/disable behavior; decide listener
      disposal, concurrent configuration and remote-failure policy before migration.
    contracts:
      exports:
        summary: >-
          CommonJS exports the wrapper class with static async init(), instance and initialised
          fields. init resolves without returning the instance. The implementation has a named
          TypeScript ActivityPubService export; the root has no shutdown method.
        sources:
          - core/server/services/activitypub/index.js
          - core/server/services/activitypub/activity-pub-service-wrapper.js
          - core/server/services/activitypub/activity-pub-service.ts
        tests: []
      construction:
        summary: >-
          The wrapper is the only production constructor found. Its first init with an available
          identity-token instance constructs ActivityPubService with shared knex and logging,
          a new URL from the site URL and the existing identity-token instance, then registers
          three settings listeners. An existing instance makes later init calls return immediately.
        sources:
          - core/server/services/activitypub/activity-pub-service-wrapper.js
          - core/server/services/activitypub/activity-pub-service.ts
          - core/server/data/db/index.js
        tests: []
      consumers:
        summary: >-
          Background boot is the only production root caller found. Settings model edits invoke
          the registered configuration callback through the shared event emitter. The separate
          webhooks service reads the persisted webhook rows and dispatches post events without
          importing this root. Direct implementation construction outside the wrapper is limited
          to unit tests in the searched repository.
        sources:
          - core/boot.js
          - core/server/services/activitypub/activity-pub-service-wrapper.js
          - core/server/models/settings.js
          - core/server/models/base/plugins/events.js
          - core/server/services/webhooks/listen.js
          - core/server/services/webhooks/webhook-trigger.js
        tests: []
      startup:
        summary: >-
          Boot initializes identity tokens before readiness, then starts unawaited background
          work which skips this root in test environments. Background boot awaits wrapper init
          after earlier recovery and scheduling. Missing identity tokens log an error and leave
          the instance unset, allowing a later explicit init attempt. Successful wrapper init
          launches configuration without awaiting enable/disable, so completion is not remote
          readiness and does not delay the server's readiness notification.
        sources:
          - core/boot.js
          - core/server/services/activitypub/activity-pub-service-wrapper.js
        tests: []
      lifetime:
        summary: >-
          The module retains the instance and initialised flag without reset or disposal.
          initialised changes only after an enable/disable promise fulfills; it is not a pending
          task guard. Settings callbacks can overlap. Repeated direct initialiseWebhooks calls
          retain existing rows when count, event, target URL and secret match; the unit test
          asserts row equality, not wrapper identity or repeated-boot behavior. Reconciliation
          ignores api_version, although inserted rows include it.
        sources:
          - core/server/services/activitypub/activity-pub-service-wrapper.js
          - core/server/services/activitypub/activity-pub-service.ts
        tests:
          - test/unit/server/services/activitypub/activity-pub-service.test.ts
      configuration:
        summary: >-
          The service captures the site URL and dependency references at construction; it queries
          the owner and obtains a fresh identity token for each remote request. Configuration
          callbacks read social_web_enabled from the settings cache on labs, social_web and
          is_private edits. This calculated setting depends on the UI setting, privacy, limits,
          subdirectory and production host checks. Helper tests cover settings, privacy, limits, subdirectory and localhost cases
          separately; they do not establish IP/IPv6 cases, wrapper events, URL capture or
          changes during pending work.
        sources:
          - core/server/services/activitypub/activity-pub-service-wrapper.js
          - core/server/services/activitypub/activity-pub-service.ts
          - core/server/services/identity-tokens/identity-token-service.ts
          - core/server/services/settings/settings-service.js
          - core/server/services/settings-helpers/settings-helpers.js
          - core/shared/settings-cache/cache-manager.js
        tests:
          - test/unit/server/services/settings-helpers/settings-helpers.test.js
      resources:
        summary: >-
          This root borrows the shared database and identity-token service. It owns three event
          subscriptions and starts HTTP/SQL work without tracking, cancelling or draining it.
          Enable reconciles four persistent webhook rows for the existing internal integration;
          replacement deletes then inserts without a transaction. Disable removes those rows
          before requesting remote site deletion. No ActivityPub cleanup task is registered.
          Implementation tests check row count, secrets, integration IDs and a DELETE request;
          they do not establish cleanup or full webhook delivery through the application.
        sources:
          - core/server/services/activitypub/activity-pub-service-wrapper.js
          - core/server/services/activitypub/activity-pub-service.ts
          - core/server/data/schema/fixtures/fixtures.json
          - core/server/lib/common/events.js
          - core/boot.js
          - core/server/ghost-server.ts
        tests:
          - test/unit/server/services/activitypub/activity-pub-service.test.ts
      failures:
        summary: >-
          Missing integration or secret logs and resolves; owner/token/fetch/JSON errors in secret
          retrieval log and return null. Remote DELETE errors log and resolve. Neither fetch
          checks HTTP success status. Other database failures reject. Configuration rejections
          are detached from wrapper init and event emitters, reaching the process rejection
          handler rather than startup's catch. A wrapper-init rejection stops later background
          startup, after readiness. Unit tests assert missing-integration and missing-owner
          handling, but not wrapper state or remote/SQL rejection policies.
        sources:
          - core/server/services/activitypub/activity-pub-service.ts
          - core/server/services/activitypub/activity-pub-service-wrapper.js
          - core/server/lib/common/events.js
          - core/boot.js
        tests:
          - test/unit/server/services/activitypub/activity-pub-service.test.ts
    unverified: []
    blockers:
      - >-
        Add wrapper and real-boot contracts for root exports, retained identity, missing identity
        tokens, repeated initialization and init returning while remote configuration is pending.
      - >-
        Decide and test concurrent settings updates: callbacks are not serialized, so repeated
        enable calls can overlap and an opposite edit during pending work can be lost.
      - >-
        Decide restart ownership and test settings-cache listener rebinding. ActivityPub retains
        its listeners and captured URL/identity instance across same-process boots, with no
        unsubscribe or in-flight drain path. Settings-cache reinit re-appends calculated-field
        listeners after retained ActivityPub listeners, creating a source-derived risk of
        reading the prior social_web_enabled value; reproduce this ordering before migration.
      - >-
        Decide failure and retry semantics. Fulfilled no-op enable can mark initialised true
        without webhooks; a fulfilled failed remote disable clears it. Neither non-success HTTP
        responses nor detached SQL rejections have wrapper-level characterization.
      - >-
        Add failure-atomicity and shutdown tests for webhook replacement and pending HTTP/SQL
        work. This root supplies no transaction, request abort signal, explicit timeout or retry.
      - >-
        Characterize reconciliation ignoring api_version. Implementation tests do not assert
        every webhook event/target/API version, disable's
        combined deletion order, real identity-token wiring or post-event delivery through the
        application. Existing row/helper tests do not make this root migration-ready.
  adapter-manager:
    kind: directory
    disposition: split
    status: audited
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: complex
      modelImpact: fits
      reason: >-
        One retained manager fits; the difficult work is moving eager adapter lookups behind
        its readiness barrier across image helpers, scheduling, settings and other
        consumers. Preserve lazy per-adapter caching and keep repeated configuration
        validation distinct from one-time manager construction. Nested adapter lifetime is
        owned separately and does not itself require replacing the manager.
      reviewedAt: f276513ba32b7114fd5cc4f7b02d3f82c0c31e3e
      sources:
        - core/server/services/adapter-manager/index.ts
        - core/kernel/adapter-manager/adapter-manager.ts
        - core/server/lib/image/index.js
        - core/server/services/post-scheduling/index.ts
        - core/boot.js
      dependencies:
        complexity: complex
        reason: >-
          The manager already accepts config, base classes and a loader, but its supporting modules
          share config and Node resolution with the build-time validator. Image helpers and
          scheduling also acquire adapters during import. Moving the singleton behind readiness must
          preserve those callers, lazy adapter loading and cached identities.
        preparation:
          - >-
            Move eager getAdapter calls in image and scheduling composition behind adapter-manager
            readiness before adopting a guarded service getter.
        reviewedAt: 44ad405bff4a8700af2ed51f833ebc08d25dddff
        sources:
          - core/server/services/adapter-manager/index.ts
          - core/kernel/adapter-manager/adapter-manager.ts
          - core/server/services/adapter-manager/adapter-paths.ts
          - core/server/services/adapter-manager/base-classes.ts
          - core/kernel/adapter-manager/loader.ts
          - core/kernel/adapter-manager/options.ts
          - core/server/services/adapter-manager/config.ts
          - bin/validate-adapters.ts
          - core/server/lib/image/index.js
          - core/server/services/post-scheduling/index.ts
    entryPoints:
      - adapter-manager/index.ts
    notes: >-
      Split in progress. The reusable loading, validation and instance-management machinery
      now lives in core/kernel/adapter-manager and takes its configuration through an
      explicit getConfig callback, so it imports nothing from the application. This
      directory holds only Ghost's composition: the base-class registry, search paths, the
      legacy scheduling/storage normalization, import-storage isolation and content-path
      defaults, and the configured instance, with live resolution on each lookup. The
      remaining relocation is of that composition into boot. The dependency audit still
      counts the registry and path files as implementation because the service rule treats
      every non-root file as one; a composition-only directory has no implementation to
      isolate, which a boot home would resolve. Reassess initialization after that move
      rather than migrating this root in place. Preserve boot validation, lazy adapter
      construction, lookup precedence, live configuration resolution and cached identity.
      Consumers retain adapters outside the manager; disposal and replacement must account
      for those references and adapter-specific resource owners.
    contracts:
      exports:
        summary: >-
          TypeScript default-exports an eager AdapterManager instance; CommonJS callers read
          .default. init and clearCache return void; getAdapter synchronously returns the
          configured adapter. Methods depend on their receiver. The implementation class,
          paths, registry and resolver utilities are deep-imported by tests and the validation
          CLI. There is no manager shutdown or disposal API.
        sources:
          - core/server/services/adapter-manager/index.ts
          - core/kernel/adapter-manager/adapter-manager.ts
          - core/kernel/adapter-manager/types.ts
          - bin/validate-adapters.ts
        tests:
          - test/unit/server/services/adapter-manager/route-settings-wiring.test.ts
      construction:
        summary: >-
          The root is the only production manager constructor found. It captures shared config,
          require, import-time lookup paths and seven registered base classes. getAdapter
          resolves and loads the class before checking its name/class cache, then constructs
          an uncached adapter and validates inheritance and required methods before caching it.
          Manager and CLI accept a matching superclass name when instanceof fails, preserving
          compatibility with another loaded copy of the base package.
          Different feature names receive separate instances even for the same class/options.
          The validation CLI shares loading helpers and instantiates bases, not this manager
          or configured adapters.
        sources:
          - core/server/services/adapter-manager/index.ts
          - core/kernel/adapter-manager/adapter-manager.ts
          - core/server/services/adapter-manager/adapter-paths.ts
          - core/server/services/adapter-manager/base-classes.ts
          - core/kernel/adapter-manager/loader.ts
          - core/server/services/adapter-manager/config.ts
          - bin/validate-adapters.ts
        tests:
          - test/unit/kernel/adapter-manager/adapter-manager.test.ts
          - test/unit/server/services/adapter-manager/base-classes.test.ts
          - test/unit/server/services/adapter-manager/adapter-paths.test.ts
      consumers:
        summary: >-
          Boot validates adapters and starts scheduling. Services retain image/settings/public
          caches, scheduling wrappers, storage/import helpers, route/redirect stores, SSO-bound
          middleware and the jobs backend. Upload endpoints, image middleware, editor helpers,
          image imports and the content-import stager also resolve adapters at use time.
          Frontend site construction retains storage serve middleware. The cache compatibility
          helper delegates to this manager but has no production caller found. The CLI consumes
          deep helpers independently; no alternate runtime manager construction was found.
          Cited wiring tests cover route stores, not the complete consumer list; the S3
          integration suite runs only when GHOST_TEST_S3_AVAILABLE is 1.
        sources:
          - core/boot.js
          - core/server/lib/image/index.js
          - core/server/lib/lexical.js
          - core/server/lib/mobiledoc.js
          - core/server/api/endpoints/authentication.js
          - core/server/api/endpoints/images.js
          - core/server/api/endpoints/media.js
          - core/server/api/endpoints/files.js
          - core/frontend/web/site.js
          - core/frontend/web/middleware/handle-image-sizes.js
          - core/server/services/settings/settings-service.js
          - core/server/services/themes/validate.js
          - core/server/services/stats/index.ts
          - core/server/services/posts-public/index.ts
          - core/server/services/tags-public/index.ts
          - core/server/services/link-redirection/index.ts
          - core/server/services/post-scheduling/index.ts
          - core/server/auth/session/index.js
          - core/server/services/route-settings/index.js
          - core/server/services/custom-redirects/index.js
          - core/server/services/media-inliner/index.ts
          - core/server/services/oembed/service.js
          - core/server/services/jobs-service/index.ts
          - core/server/services/members/import-export/index.ts
          - core/server/services/content-import/index.ts
          - core/server/data/importer/index.js
          - core/server/data/importer/content-files.ts
          - core/server/data/importer/handlers/image.js
          - core/server/adapters/cache/index.js
          - bin/validate-adapters.ts
        tests:
          - test/unit/server/services/adapter-manager/route-settings-wiring.test.ts
          - test/integration/adapters/route-settings/adapter-manager-wiring.test.ts
      startup:
        summary: >-
          initCore calls synchronous init before subsequent core/service setup. Every init
          enumerates configured types/features, deduplicates type/class/serialized-options
          combinations and loads classes to call optional synchronous static validate. It
          neither constructs instances nor checks required instance methods; module loading
          can itself have effects. Unconfigured types are skipped. getAdapter has no init
          guard and import-time consumers can construct adapters independently of validation.
        sources:
          - core/boot.js
          - core/kernel/adapter-manager/adapter-manager.ts
          - core/kernel/adapter-manager/options.ts
          - core/server/services/post-scheduling/index.ts
          - core/server/api/endpoints/authentication.js
          - core/server/lib/image/index.js
        tests:
          - test/unit/kernel/adapter-manager/adapter-manager.test.ts
          - test/unit/server/services/adapter-manager/route-settings-wiring.test.ts
      lifetime:
        summary: >-
          Module and instance caches retain manager/adapters across same-process boots. init
          revalidates without clearing or refreshing instances. Cache keys include the full
          requested name and class, not options; switching away and back can reuse old state.
          clearCache only drops manager references and has no production call found; retained
          consumers, Node module cache and the Redis store singleton remain independent.
          Conditional S3 route-store integration tests clear the cache before rereading settings;
          they do not establish replacement of retained consumers or cleanup.
        sources:
          - core/kernel/adapter-manager/adapter-manager.ts
          - core/server/lib/image/index.js
          - core/server/services/jobs-service/index.ts
          - core/server/adapters/lib/redis/redis-store-factory.js
          - core/server/ghost-server.ts
        tests:
          - test/unit/kernel/adapter-manager/adapter-manager.test.ts
          - test/integration/adapters/route-settings/adapter-manager-wiring.test.ts
          - test/unit/server/adapters/lib/redis/redis-store-factory.test.ts
      configuration:
        summary: >-
          Selection/options are resolved on each call, but same-key cached instances keep
          construction-time state. Lookup paths are captured at module import in node_modules,
          internal, installed and content order, with duplicates removed. Normalization
          preserves legacy storage/scheduling config, local imports defaults and route/redirect
          paths. Feature objects merge over shared options; a string feature without its
          class config falls back to active. Config-resolution tests use stubs or test-only
          config mutation; they establish neither production hot reload nor changed options
          reaching an already retained adapter.
        sources:
          - core/kernel/adapter-manager/adapter-manager.ts
          - core/server/services/adapter-manager/adapter-paths.ts
          - core/kernel/adapter-manager/options.ts
          - core/server/services/adapter-manager/config.ts
          - core/shared/config/defaults.json
        tests:
          - test/unit/kernel/adapter-manager/adapter-manager.test.ts
          - test/unit/kernel/adapter-manager/options.test.ts
          - test/unit/server/services/adapter-manager/config.test.ts
          - test/unit/server/services/adapter-manager/adapter-paths.test.ts
          - test/integration/adapters/storage/imports-storage-config.test.ts
      resources:
        summary: >-
          The manager owns instance references, with no generic disposal contract. Nested
          adapters own memory/TTL cache state, Redis client listeners and pending reads/refreshes,
          filesystem work, S3 clients, scheduler queues/timers and jobs queues/recurring timers.
          Redis may reuse a process-wide store across feature instances. SchedulingDefault
          has no stop method; S3/Redis adapters expose no cleanup here. JobsService separately
          owns backend shutdown through GhostServer cleanup. clearCache invokes none of these
          owners and cannot drain active consumers or release custom-adapter resources.
        sources:
          - core/kernel/adapter-manager/adapter-manager.ts
          - core/kernel/adapter-manager/types.ts
          - core/server/adapters/cache/MemoryCache.js
          - core/server/adapters/cache/AdapterCacheMemoryTTL.js
          - core/server/adapters/lib/redis/AdapterCacheRedis.js
          - core/server/adapters/lib/redis/redis-store-factory.js
          - core/server/adapters/storage/LocalStorageBase.ts
          - core/server/adapters/storage/S3Storage.ts
          - core/server/adapters/route-settings/S3RouteSettingsStore.ts
          - core/server/adapters/redirects/S3RedirectsStore.ts
          - core/server/adapters/scheduling/scheduling-default.ts
          - core/server/adapters/jobs/InMemoryJobsBackend.ts
          - core/server/services/jobs-service/index.ts
          - core/boot.js
          - core/server/ghost-server.ts
        tests:
          - test/unit/server/adapters/lib/redis/redis-store-factory.test.ts
          - test/unit/server/adapters/jobs/in-memory-jobs-backend.test.ts
      failures:
        summary: >-
          getAdapter synchronously throws for unknown names, loader failures, invalid inheritance
          and missing required methods; constructor failures propagate. Invalid instances are
          not cached but can already have allocated resources, with no compensating cleanup.
          Even cached retrieval first resolves/loads and can fail. init aggregates load/static
          validation failures; earlier normalization/serialization failures escape directly.
          Static validation is synchronous and not awaited. No failed-init guard, rollback or
          manager retry exists; later calls retry uncached work using current config.
        sources:
          - core/kernel/adapter-manager/adapter-manager.ts
          - core/kernel/adapter-manager/loader.ts
          - core/server/services/adapter-manager/config.ts
          - core/kernel/adapter-manager/types.ts
        tests:
          - test/unit/kernel/adapter-manager/adapter-manager.test.ts
          - test/unit/server/services/adapter-manager/adapter-paths.test.ts
          - test/unit/server/services/adapter-manager/route-settings-wiring.test.ts
    unverified: []
    blockers:
      - >-
        Add same-manager contracts for options-only edits, class switching back, import-time
        path capture, loader failures on a cached key, repeated init and clearCache identity.
        Existing tests cover different-class selection and feature identity, not these retained
        state boundaries; normalization alone does not update a constructed adapter.
      - >-
        Characterize actual retained consumers across clearCache and same-process boot,
        especially shared image/public caches, scheduling/auth, SSO middleware and jobs.
        The jobs retained-consumer test stubs getAdapter, so it does not prove manager wiring.
      - >-
        Establish ownership and cleanup for scheduler timers, Redis shared connections and
        per-instance listeners, TTL timers, S3 clients and custom adapters before replacing
        instances. Preserve JobsService's existing backend shutdown owner; cache clearing is
        not disposal and must not silently strand active work or duplicate shared resources.
      - >-
        Cover construction or required-method validation failing after allocation, validation
        recovery, and boot failure with already retained adapters. Current static-validation
        tests do not assert absence of constructor/resource effects or real boot ordering.
      - >-
        Preserve root CommonJS/default interoperability, receiver requirements and the CLI's
        deep shared loader/registry contract. Keep required-method validation on construction
        distinct from optional static boot validation; neither proves remote readiness.
  app-installations:
    kind: directory
    disposition: migrate
    status: auditing
    migrated: true
    extras:
      actingContext:
        purpose: Convert API authentication context into the staff actor recorded in installation history.
        callers:
          - core/server/api/endpoints/app-installations.ts
        timing: Pure conversion when an API command runs; it does not require initialization.
        dependencies: []
        failurePolicy: A missing staff user or an integration yields a null actor; permission checks remain with the API.
        destination: A normalized request actor supplied by the API framework Frame, with actor policy owned by the application.
    migration:
      complexity: straightforward
      modelImpact: fits
      reason: >-
        The guarded boot factory still composes one instance after database readiness. It
        holds no resources, but it keeps the manifest re-checks in flight, which starting
        app sessions together relies on to share one fetch, so it must stay the one retained
        instance rather than be composed per request. Its repository holds only the knex
        handle it's built from, and the response serializers are pure. Return that instance
        through the initializer and update endpoint access, preserving live
        manifest/configuration callbacks. The new history, detail, uninstall and app session
        behavior is per operation and adds no startup lifecycle.
      reviewedAt: 166037f8c6c59c3e7fa63e49c8a5a35101f31d67
      sources:
        - core/server/services/app-installations/index.ts
        - core/server/services/app-installations/service.ts
        - core/server/services/app-installations/app-installations-repository.ts
        - core/server/services/app-installations/serializers.ts
        - core/server/services/app-installations/fetch-icon.ts
        - core/server/services/app-installations/decide-on-manifest.ts
        - core/server/services/app-installations/history.ts
        - core/server/api/endpoints/app-installations.ts
        - core/server/api/endpoints/app-sessions.ts
        - core/boot.js
    entryPoints:
      - app-installations/index.ts
    notes: >-
      Composition-only service over the app installation tables, which have migrations and
      exist in every database; the private `apps` flag keeps the routes dormant. Root units
      now cover deferred database acquisition, retained identity, failure/retry, earlier-loaded
      controllers and live manifest rules/localhost aliases with a fake outbound transport.
    contracts:
      exports:
        summary: >-
          The root default-exports the shared initializer with async init and a guarded
          service getter and a typed actingContext extra. There are no named runtime
          exports. Reading service before readiness throws.
        sources:
          - core/server/services/app-installations/index.ts
        tests:
          - test/e2e-api/admin/app-installations.test.ts
          - test/unit/server/services/app-installations/index.test.ts
      construction:
        summary: >-
          Boot awaits initialization after database setup. The factory returns one
          AppInstallationsService from knex, the Action model and a manifest loader built
          from the outbound request guard, a manifest-rules getter and the localhost alias.
          The root supplies logging to manifest loading and action recording. Successful
          initialization retains that ready instance. Root units prove no connection access
          on import, one retained instance without queries/delivery, and no publication after
          a failed connection acquisition followed by a successful retry. They also verify
          logged transport failures keep generic public errors and logged action-write failures
          do not fail installation. The retained instance also holds the in-flight re-check
          per installation that app sessions starting together share; e2e tests cover two
          sessions making one fetch.
        sources:
          - core/server/services/app-installations/index.ts
          - core/server/services/app-installations/service.ts
          - core/server/services/app-installations/load-manifest.ts
          - core/boot.js
        tests:
          - test/e2e-api/admin/app-installations.test.ts
          - test/unit/server/services/app-installations/index.test.ts
      configuration:
        summary: >-
          The site and Admin URLs and the development flag are read every time a manifest
          is loaded (preview, install, approval and the re-check at app session start), not
          captured at construction. Root units
          change development aliases, environment and publication URLs on one instance and
          assert current preview routing/validation.
        sources:
          - core/server/services/app-installations/index.ts
          - core/server/services/app-installations/load-manifest.ts
        tests:
          - test/e2e-api/admin/app-installations.test.ts
          - test/unit/server/services/app-installations/index.test.ts
    unverified:
      - consumers
      - startup
      - lifetime
      - resources
      - failures
    blockers: []
  audience-feedback:
    kind: directory
    disposition: migrate
    status: audited
    migrated: true
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: straightforward
      modelImpact: fits
      reason: >-
        Compose and publish the repository, service and controller together, retaining their
        identities. Boot must await this root before email rendering captures its service;
        request consumers should read the ready composition. Constructors acquire no
        subscriptions or background work, so the existing initializer is sufficient; avoid
        the current partial publication before controller construction.
      reviewedAt: f276513ba32b7114fd5cc4f7b02d3f82c0c31e3e
      sources:
        - core/server/services/audience-feedback/index.ts
        - core/server/services/audience-feedback/audience-feedback-service.js
        - core/server/services/email-service/email-service-wrapper.js
        - core/server/web/members/app.js
        - core/boot.js
      dependencies:
        complexity: straightforward
        reason: >-
          The root already supplies models and URL dependencies to its repository and service.
          Remaining implementation imports are the permissions facade in the controller and toPlain
          in link construction; both have small injection seams. Email rendering captures the ready
          service, while the members router needs the completed controller.
        preparation:
          - >-
            Inject permissions and toPlain from index.ts and publish repository, service and
            controller as one completed composition.
          - >-
            Await the root before Email captures its service and before the members router consumes
            its controller.
        reviewedAt: 44ad405bff4a8700af2ed51f833ebc08d25dddff
        sources:
          - core/server/services/audience-feedback/index.ts
          - core/server/services/audience-feedback/audience-feedback-controller.js
          - core/server/services/audience-feedback/audience-feedback-service.js
          - core/server/services/email-service/email-service-wrapper.js
          - core/server/web/members/app.js
    entryPoints:
      - audience-feedback/index.ts
    notes: >-
      Preserve the repository, service and controller surfaces, captured fallback URL and
      retained email-renderer dependency. Keep feedback rules and persistence outside HTTP;
      the existing controller also contains an Express redirect adapter. Initializer wiring was
      rechecked against 2196fa4e4cf4ef4ecf41df16cfe9f460270cadf5; the original contract audit and
      migration-assessment revisions above remain historical evidence. Permissions is now
      root-injected. The earlier proposal to inject toPlain is superseded by direct use of the
      stateless shared helper under an exact-file library allowance.
    contracts:
      exports:
        summary: >-
          Root default-exports defineService; CommonJS callers read .default. Its guarded .service
          contains the repository, link service and controller, published together only after their
          construction completes. Deep modules still export constructible classes. Link methods are
          synchronous: buildLink and buildFallbackLink return URL objects, while buildEmailLink returns
          a string. Controller and repository operations remain async and receiver-dependent. There
          are no extras, reset or service-owned shutdown hooks. Root units pin that repeated init
          retains all three composition members; the guarded getter follows the shared kernel contract.
        sources:
          - core/server/services/audience-feedback/index.ts
          - core/server/services/audience-feedback/audience-feedback-service.js
          - core/server/services/audience-feedback/audience-feedback-controller.js
          - core/server/services/audience-feedback/feedback-repository.js
          - core/server/services/audience-feedback/feedback.js
        tests:
          - test/unit/server/services/audience-feedback/audience-feedback-service.test.js
          - test/unit/server/services/audience-feedback/index.test.ts
      construction:
        summary: >-
          Import loads shared URL utilities, URL service and the permissions facade without building
          the composition or reading the home URL. Boot is the only production init caller found. The
          factory resolves shared models and constructs a model-backed repository, a link service with
          a captured home URL, then a controller retaining both plus permissions. Constructors only
          store references; the complete graph is returned for publication. No other production
          construction of these private classes was found. Root units pin no URL read on import, one
          home-URL read and no model or URL-service queries across repeated init, and successful retry
          after home-URL lookup throws.
        sources:
          - core/server/services/audience-feedback/index.ts
          - core/server/services/audience-feedback/audience-feedback-controller.js
          - core/server/services/audience-feedback/audience-feedback-service.js
          - core/server/services/audience-feedback/feedback-repository.js
          - core/server/services/url/index.js
          - core/boot.js
        tests:
          - test/unit/server/services/audience-feedback/index.test.ts
      consumers:
        summary: >-
          Email initialization passes the ready composition's link service to EmailRenderer, which
          retains it and builds both links on each render even when feedback buttons are disabled.
          Members GET redirects and Members POST/Admin GET endpoint queries retain the initializer
          and read .service.controller per call. POST authenticates by session or signed member UUID;
          GET redirect only forwards UUID/key. Admin browse requires post-browse permission and
          conditionally includes member details. No external repository consumer was found;
          getMemberByUuid has no caller. HTTP tests exercise submission and redirects. Renderer units
          inject a fake link service and pin button inclusion/exclusion and link tracking. An HTTP
          case obtains feedback links from the booted EmailRenderer's template data and follows both
          through Members redirects; root units drive the retained controller through its real
          repository and live URL routing.
        sources:
          - core/server/services/email-service/email-service-wrapper.js
          - core/server/services/email-service/email-renderer.js
          - core/server/web/members/app.js
          - core/server/services/members/middleware.js
          - core/server/api/endpoints/feedback-members.js
          - core/server/web/api/endpoints/admin/routes.js
          - core/server/services/audience-feedback/audience-feedback-controller.js
          - core/server/services/audience-feedback/feedback-repository.js
        tests:
          - test/e2e-api/members/feedback.test.js
          - test/unit/server/services/email-service/email-renderer.test.js
          - test/unit/server/services/audience-feedback/audience-feedback-controller.test.js
          - test/unit/server/services/audience-feedback/index.test.ts
      startup:
        summary: >-
          Boot initializes models, settings and routing first, then explicitly awaits this initializer
          before starting the service group that includes Email. This preserves Email's captured
          collaborator when the kernel publishes its synchronously constructed graph after awaiting
          the factory. It no longer relies on Promise.all argument evaluation completing publication
          synchronously. Members routes can be composed early because request handlers defer the
          guarded .service.controller lookup. The HTTP characterization exercises a booted Email
          renderer and both redirect paths, not deliberate startup-failure injection.
        sources:
          - core/boot.js
          - core/server/services/audience-feedback/index.ts
          - core/server/services/email-service/email-service-wrapper.js
          - core/server/web/members/app.js
          - core/server/api/endpoints/feedback-members.js
        tests:
          - test/e2e-api/members/feedback.test.js
      lifetime:
        summary: >-
          Module caching and the kernel retain the same complete graph across requests and same-process
          boots. EmailRenderer and the controller keep their original collaborator references. Feedback
          is persisted in members_feedback; each submission rereads the post and existing member/post
          row, with no local cache. A changed score mutates the fetched Feedback and writes it; the same
          score returns without a write. HTTP assertions pin stable feedback ID, changed score and
          updated_at advancing once then remaining unchanged on a sequential repeat. They do not
          establish simultaneous-write behavior. There is no transaction, lock or deduplication around
          read-then-add, and the declared schema has no unique member/post pair constraint.
        sources:
          - core/server/services/audience-feedback/index.ts
          - core/server/services/audience-feedback/audience-feedback-controller.js
          - core/server/services/audience-feedback/feedback-repository.js
          - core/server/services/email-service/email-renderer.js
          - core/server/data/schema/schema.js
        tests:
          - test/e2e-api/members/feedback.test.js
      configuration:
        summary: >-
          init captures a derived home URL from shared URL utilities; audience feedback does
          not read an application-config facade per operation or refresh that base on re-init.
          buildLink normalizes model/plain input, supplies type posts to the retained URL
          service, and uses the captured base when its URL contains /404/. Redirect fetches the
          current post with tags/authors; absent posts bypass URL lookup. Hash links encode
          UUID/key. Email links use post ID under the captured base/subdirectory and preserve
          literal member placeholders. Units pin these URL shapes, fallback and model input;
          HTTP redirects check Location patterns without exercising slug/routing changes.
          Browse defaults limit/page to 10/1, parses score and always orders newest first;
          requested order is not forwarded. Root units pin init-time capture: changing the home
          URL after init and re-init leaves email and fallback links on the captured base.
        sources:
          - core/server/services/audience-feedback/index.ts
          - core/server/services/audience-feedback/audience-feedback-service.js
          - core/server/services/audience-feedback/audience-feedback-controller.js
          - core/server/services/audience-feedback/feedback-repository.js
          - core/shared/url-utils.ts
          - core/server/lib/common/to-plain.ts
          - core/server/services/url/lazy-url-service.ts
          - core/server/api/endpoints/feedback-members.js
        tests:
          - test/unit/server/services/audience-feedback/audience-feedback-service.test.js
          - test/unit/server/services/audience-feedback/audience-feedback-controller.test.js
          - test/unit/server/services/audience-feedback/feedback-repository.test.js
          - test/e2e-api/members/feedback.test.js
          - test/unit/server/services/audience-feedback/index.test.ts
      resources:
        summary: >-
          This graph owns references and a URL, with no timer, subscription, provider client or
          shutdown hook. Repository operations borrow shared models/database; URL routing state
          and permission state belong to their shared dependencies. No feedback operation is
          tracked, cancelled or drained by this wrapper, and add/edit do not propagate caller
          transaction options. MemberFeedback forbids direct destroy, while schema foreign keys
          declare deletion with the member/post. Other subsystems read these rows directly for
          activity and post/member analytics; they do not own a feedback-service instance.
          Model tests pin missing-field validation and disabled destroy, not cleanup, cascades,
          transaction behavior or pending-query ownership.
        sources:
          - core/server/services/audience-feedback/index.ts
          - core/server/services/audience-feedback/feedback-repository.js
          - core/server/services/url/index.js
          - core/server/models/member-feedback.js
          - core/server/models/base/plugins/crud.js
          - core/server/data/schema/schema.js
          - core/server/models/post.js
          - core/server/models/member.js
          - core/server/services/members/members-api/repositories/event-repository.js
        tests:
          - test/unit/server/models/member-feedback.test.ts
      failures:
        summary: >-
          Factory failure rejects init without publishing a partial composition; the kernel leaves
          .service unavailable and permits a later explicit retry. Locally constructed repositories
          only borrow models and acquire no resources requiring rollback. Root units pin propagation
          of a home-URL lookup failure and successful later init; later constructor failure is not
          fault-injected. Link building still propagates serialization/URL failures. Redirect catches
          errors and passes them to next; a unit pins repository rejection identity and no redirect.
          Add rejects invalid numeric score, absent member context or missing post, otherwise propagating
          repository/ID-conversion failures; there is no retry or compensation. Browse swallows any
          permission-check failure and continues without member data, but propagates query failures.
          Units pin allowed/denied browse delegation. HTTP tests pin invalid-score 422, missing-post 404
          and authentication 401; malformed/unknown UUID cases omit the key and fail before member
          lookup. Write/read failures and concurrent submissions lack direct coverage.
        sources:
          - core/server/services/audience-feedback/index.ts
          - core/server/services/audience-feedback/audience-feedback-controller.js
          - core/server/services/audience-feedback/audience-feedback-service.js
          - core/server/services/audience-feedback/feedback-repository.js
          - core/server/services/audience-feedback/feedback.js
          - core/server/services/members/middleware.js
        tests:
          - test/unit/server/services/audience-feedback/audience-feedback-controller.test.js
          - test/e2e-api/members/feedback.test.js
          - test/unit/server/services/audience-feedback/index.test.ts
    unverified: []
    blockers:
      - >-
        Root units pin repeated-init retention, init-time URL capture and retry after a
        home-URL failure. The shared kernel owns guarded access and publication; service-specific
        receiver requirements, pre-init access and later-constructor failure lack direct tests.
        The booted Email renderer HTTP case covers successful dependency readiness only.
      - >-
        Characterize simultaneous submissions and failing writes. Read-then-add has no uniqueness
        or transaction protection, so sequential ID/timestamp assertions do not rule out duplicate
        rows or conflicting updates. Pin repository model calls, ID conversion, transaction and
        pending-query ownership before changing construction or promising atomicity/cleanup.
      - >-
        Cover the real Admin browse boundary, member-data privacy, pagination/score parsing,
        ignored order and model-result mapping; existing repository tests only use empty results.
        Test validly signed unknown members, missing context and provider/model failures separately
        from current missing-key authentication cases.
      - >-
        Pin a real rendered newsletter link through redirect and submission, including changed
        slugs/routes, non-published/missing posts, subdirectory bases and encoded credentials.
        Unit URL tests and two HTTP Location patterns do not establish that whole contract or
        same-process restart/config capture. Keep any new reload behavior separate from migration.
  automations:
    kind: directory
    disposition: migrate
    status: audited
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: complex
      modelImpact: fits
      reason: >-
        The retained poller fits, but its two subscriptions, timer and scheduler
        registration need failure cleanup before publication, while the API owns a separate
        eager repository. Preserve startup after jobs are ready and separate first-poll work
        from readiness. Retained API and analytics consumers must share the chosen
        composition; these are ownership and ordering changes, not evidence that the helper
        needs scheduler hooks.
      reviewedAt: f276513ba32b7114fd5cc4f7b02d3f82c0c31e3e
      sources:
        - core/server/services/automations/index.ts
        - core/server/services/automations/service.ts
        - core/server/services/automations/automations-api.ts
        - core/server/lib/soonest-timer.ts
        - core/boot.js
      dependencies:
        complexity: complex
        reason: >-
          The root exposes a poller while automations-api independently constructs repositories and
          reads config, Tinybird and settings. Polling reaches member-welcome-emails and
          email-analytics, while members, link tracking and endpoints retain the separately composed
          API. Root-only wiring requires unifying these composition paths and preserving late access
          to other services.
        preparation:
          - >-
            Turn the API/repository composition into a factory and inject it into the poller,
            members signup, analytics and link tracking instead of retaining a second eager graph.
          - >-
            Inject member lookup, welcome-email sending, analytics scheduling and live
            Tinybird/settings providers into polling and repository helpers; keep current
            configuration-read timing.
          - >-
            Wire scheduler helpers, logging and timer utilities at the root and use the existing
            kernel clock for legacy Moment operations.
        reviewedAt: 44ad405bff4a8700af2ed51f833ebc08d25dddff
        sources:
          - core/server/services/automations/index.ts
          - core/server/services/automations/service.ts
          - core/server/services/automations/automations-api.ts
          - core/server/services/automations/database-automations-repository.ts
          - core/server/services/automations/poll.ts
          - core/server/services/automations/welcome-email-automation-poll.ts
          - core/server/services/members/members-api/members-api.js
          - core/server/services/link-tracking/index.js
          - core/boot.js
    entryPoints:
      - automations/index.ts
      - automations/automations-api.ts
    notes: >-
      Manage both the polling instance and the separately composed API repository; preserve
      scheduling, automation notifications, legacy welcome runs and retained analytics/click consumers.
      Audited means investigated, not covered or ready to migrate.
    contracts:
      exports:
        summary: >-
          The root exports one named automationsService. Its receiver-dependent init returns
          synchronously; rescheduleAll and the test enqueue method return promises, but immediate
          enqueue completion means an event was dispatched, not that polls finished. The separate
          API exports receiver-independent async reads, graph edits, event notifications, step and
          recipient operations; requestPoll synchronously dispatches and returns undefined.
          Repository methods retain injected Knex/configuration in closures. API units assert
          selected validated arguments, count forwarding and add result identity with substituted
          repository methods. Add now validates the submitted status and action graph, including
          active email content, before repository insertion; units reject selected invalid graphs
          and content without calling the repository. Admin HTTP and repository integration
          assert that an active automation retains its submitted actions and edges on reread.
          The signed polling HTTP test asserts 204 and one dispatch through
          a dispatch stub, not completed work or the complete export/receiver contract.
        sources:
          - core/server/services/automations/index.ts
          - core/server/services/automations/service.ts
          - core/server/services/automations/automations-api.ts
          - core/server/services/automations/database-automations-repository.ts
          - core/server/api/endpoints/automations.ts
        tests:
          - test/unit/server/services/automations/automations-api.test.ts
          - test/unit/server/services/automations/service.test.ts
          - test/e2e-api/admin/automations.test.ts
          - test/integration/services/automations/database-automations-repository.test.ts
      construction:
        summary: >-
          Loading the root constructs AutomationsService and imports the separately cached API,
          which immediately constructs its repository with the shared Knex handle and parsed
          fakeWaitHoursMultiplier. It also loads shared request transport and other wrappers.
          First init constructs a SoonestTimer, two independently serialized event handlers and
          scheduler registration, launches an unawaited immediate enqueue, then publishes its
          private guard. No DB query or poll is required to finish for publication. Subscribing or
          registering can fail after earlier subscriptions exist, without rollback. API stats
          calls construct short-lived Tinybird clients; the service passes the ready
          welcome-email instance to both pollers. Units construct the class with fake events/scheduler and check
          two subscriptions after repeated init; they do not assert root import acquisition,
          retained collaborator identity or partial-init cleanup/retry.
        sources:
          - core/server/services/automations/index.ts
          - core/server/services/automations/service.ts
          - core/server/services/automations/automations-api.ts
          - core/server/services/automations/database-automations-repository.ts
          - core/server/services/member-welcome-emails/index.ts
          - core/server/lib/request-external.js
        tests:
          - test/unit/server/services/automations/service.test.ts
      consumers:
        summary: >-
          Boot retains the root and injects the API into email analytics. Admin automation,
          statistics, run/history and action-link controllers import that API directly; member
          repository and link tracking retain it for signup and transactional click recording.
          Scheduler registration retains the service. Member signup captures member/tier data:
          with an outer transaction, new automation creation runs in a detached post-commit
          callback; otherwise it is awaited. Legacy signup writes its run in the caller's
          transaction and dispatches after commit. Member-flow tests use the real member API,
          signed poll HTTP, subscriptions, DB and renderer, asserting two email payloads/counts
          while replacing scheduler/provider sends. Analytics integration asserts persisted
          delivery/open results with provider events and cursors substituted. These establish
          selected wiring, not replacement compatibility or a durable signup-to-run outbox.
        sources:
          - core/server/lib/after-commit.ts
          - core/boot.js
          - core/server/api/endpoints/automations.ts
          - core/server/api/endpoints/automation-performance-stats.ts
          - core/server/api/endpoints/automation-runs.ts
          - core/server/api/endpoints/automation-run-history.ts
          - core/server/api/endpoints/automation-action-links.ts
          - core/server/services/members/members-api/members-api.js
          - core/server/services/members/members-api/repositories/member-repository.js
          - core/server/services/link-tracking/index.js
          - core/server/services/link-tracking/link-click-tracking-service.js
          - core/server/services/email-analytics/index.ts
          - core/server/services/email-analytics/automation-email-analytics-batch-processor.ts
        tests:
          - test/e2e-api/automations/automations.test.ts
          - test/unit/server/services/members/members-api/repositories/member-repository.test.js
          - test/integration/services/email-analytics/automation-email-analytics.test.js
      startup:
        summary: >-
          Boot loads both compositions during initServices and calls automations init after
          dependent service initialization, job-handler registration and awaited jobs.start,
          before conditional post rescheduling. Boot's await of synchronous init cannot establish
          first-poll completion. Both new and legacy pollers are subscribed regardless of the
          automations flag; that flag chooses signup enrollment paths, not whether persisted
          work is processed. Browsing the repository also creates missing default automations
          and copies eligible legacy email content, so it is not a read-only readiness check.
          Boot units hold jobs startup and assert synchronous init error identity/short-circuit;
          real boot HTTP holds jobs startup and asserts maintenance before automation init.
          Service units assert deferred dispatch, and Admin HTTP checks default creation/copying.
          None asserts failed first-poll recovery or first-poll completion as readiness.
        sources:
          - core/boot.js
          - core/server/services/automations/service.ts
          - core/server/services/automations/automations-api.ts
          - core/server/services/automations/database-automations-repository.ts
          - core/server/services/members/members-api/repositories/member-repository.js
        tests:
          - test/unit/boot/init-services.test.ts
          - test/e2e-server/boot-readiness.isolated.test.ts
          - test/unit/server/services/automations/service.test.ts
          - test/e2e-api/admin/automations.test.ts
      lifetime:
        summary: >-
          Successful init keeps its original callbacks, scheduler, key owner and timer; there is
          no reset, stop or restart hook. The separate repository persists for module lifetime;
          persisted runs, revisions and claim markers survive process replacement. New steps use 30-minute claims,
          lock-token-checked updates and transactional completion/next-step insertion; next
          actions use the current graph/revision, not a whole-run snapshot. Enrollment locks the
          member and avoids a second run for the same automation/member. Real DB integration
          holds locks to test same-member duplicate prevention and different-member concurrency;
          selected-tier fetch tests are sequential. SQLite repository tests assert stale/fresh
          claim handling and refusal of an obsolete lock owner, not multi-process execution.
          Same-process restart, legacy concurrent claiming, crash recovery, long sends exceeding
          claims and automatic wakeup solely for a fresh lock's expiry remain unpinned.
        sources:
          - core/server/services/automations/service.ts
          - core/server/services/automations/database-automations-repository.ts
          - core/server/services/automations/welcome-email-automation-poll.ts
          - core/server/services/automations/constants.ts
          - core/server/services/automations/stale-lock-cutoff.ts
        tests:
          - test/unit/server/services/automations/service.test.ts
          - test/unit/server/services/automations/database-automations-repository.test.ts
          - test/integration/services/automations/database-automations-repository.test.ts
      configuration:
        summary: >-
          API import captures the parsed positive integer fake wait multiplier, otherwise wait
          hours use real milliseconds. Init captures API URL and site UUID/API-URL fallback for
          scheduler jitter, but retrieves the current scheduler key for each future enqueue.
          Polls read current member status/preferences, action revision and settings; click
          tracking uses email_track_clicks, while opens use
          email_track_opens and recipient open tracking additionally requires a Mailgun ID.
          API stats checks current Tinybird config, wrapper instance/token and
          site ID; shared request agents and frozen URL helpers retain earlier configuration.
          Welcome-email caches/transports belong to that wrapper. Parser/timing units assert
          value validation and cadence; fake poll units assert tracking arguments. HTTP stats
          tests use test configuration overrides/provider responses, not production hot reload
          or full captured-versus-accessor replacement coverage.
        sources:
          - core/server/services/automations/automations-api.ts
          - core/server/services/automations/fake-wait-hours-multiplier.ts
          - core/server/services/automations/service.ts
          - core/server/services/automations/scheduler-poll-time.ts
          - core/server/services/automations/poll.ts
          - core/server/services/automations/database-automations-repository.ts
          - core/server/services/member-welcome-emails/index.ts
          - core/server/services/stats/utils/tinybird.ts
          - core/server/lib/request-external.js
          - core/shared/url-utils.ts
        tests:
          - test/unit/server/services/automations/fake-wait-hours-multiplier.test.ts
          - test/unit/server/services/automations/scheduler-poll-time.test.ts
          - test/unit/server/services/automations/poll.test.ts
          - test/e2e-api/admin/automations.test.ts
      resources:
        summary: >-
          The service owns two DomainEvents subscriptions and a referenced SoonestTimer with
          chained long delays, but stores no disposer or poll-drain handle and registers no
          GhostServer cleanup. It borrows DB, scheduler, welcome-email transports/caches and
          recurring analytics jobs; shared HTTP agents are created by request-external. Each
          poller serializes itself and coalesces at most one extra invocation; the two pollers
          can overlap. Installed DomainEvents dispatch is synchronous and its allSettled tracking
          is test-only. oneAtATime starts a queued invocation without awaiting/returning it, so
          the original promise can settle while queued work remains. Its named completion test
          holds only the first invocation. Timer units cover earliest-date replacement and long
          delays with fake time, not disposal. Tinybird requests disable retries and use a 35s
          request timeout; member search adds bounded batches, a between-batch soft budget and
          a 3s POST abort signal. No root shutdown cancels these requests or stops new polling.
        sources:
          - core/server/services/automations/service.ts
          - core/server/lib/soonest-timer.ts
          - core/shared/one-at-a-time.ts
          - core/server/services/automations/poll.ts
          - core/server/services/automations/welcome-email-automation-poll.ts
          - core/server/services/automations/automation-member-search.ts
          - core/server/services/stats/utils/tinybird.ts
          - core/server/lib/request-external.js
          - core/server/services/member-welcome-emails/index.ts
          - core/server/services/email-analytics/jobs/index.js
          - core/server/ghost-server.ts
          - package.json
        tests:
          - test/unit/server/services/automations/service.test.ts
          - test/unit/shared/one-at-a-time.test.ts
      failures:
        summary: >-
          Synchronous init failure reaches boot; later poll-level rejection is swallowed by
          oneAtATime. Scheduler/key failure logs after the local timer is armed. New polling logs
          per-step failures and continues peers; send/advance failures retry after ten minutes
          up to ten attempts, but recipient or analytics-scheduling failure is logged and allows
          completion without retrying the sent mail. Email sending is outside DB transactions:
          failure advancing an already sent step can retry its send. Legacy polling sends before
          its recipient/completion transaction, retries that failure, and discards per-run rejected
          allSettled results. Tests assert selected stubbed retry/continuation paths; legacy DB
          integration injects completion failure and asserts recipient rollback plus retry with
          fake sending. Real execution/history HTTP asserts a recorded send followed by final
          advancement failure using a fake sender. Browse falls back to SQL on unusable Tinybird
          stats, while performance/run endpoints fail; intercepted HTTP asserts fallback without
          retry and retains zero stats for a successful empty response. These do not guarantee
          exactly-once delivery, crash recovery or drain completion.
        sources:
          - core/server/services/automations/service.ts
          - core/shared/one-at-a-time.ts
          - core/server/services/automations/poll.ts
          - core/server/services/automations/welcome-email-automation-poll.ts
          - core/server/services/automations/database-automations-repository.ts
          - core/server/services/automations/automations-api.ts
          - core/server/services/automations/tinybird-automation-stats.ts
        tests:
          - test/unit/server/services/automations/poll.test.ts
          - test/integration/services/automations/welcome-email-automation-poll.test.ts
          - test/e2e-api/admin/automation-run-history-execution.test.ts
          - test/e2e-api/admin/automations.test.ts
          - test/e2e-api/admin/automation-performance-stats.test.ts
          - test/e2e-api/admin/automation-runs.test.ts
    unverified: []
    blockers:
      - >-
        Preserve both composition paths and retained callers; pin import-time repository and
        transport acquisition, synchronous init/dispatch return behavior, dependency capture,
        root identity and partial-subscription/registration failure before changing lifecycle.
        Current boot barrier/error tests already cover orchestration with substituted services.
      - >-
        Define stop-accepting, timer cancellation, subscription/scheduler ownership and in-flight
        draining before supporting reinitialization. The oneAtATime completion promise does not
        include a held queued invocation; add that assertion before relying on it for draining.
        Existing event settlement and timer tests do not establish production shutdown.
      - >-
        Preserve failure/recovery semantics across both pollers: detached post-commit signup,
        fetch/lookup/claim failure, fresh-lock recovery without another event, partial init,
        legacy concurrent claims and sends that outlive locks remain lifecycle obligations.
        Sending and completion are separate effects; test crash/advance-failure duplication
        boundaries rather than promising exactly-once provider delivery.
      - >-
        Characterize captured multiplier/URL/site identity versus accessor-based flags, keys and
        wrapper dependencies. Keep the existing API validation, statistics fallback, run history,
        tracking transactions and HTTP-owned add gates/soft count limit intact during migration;
        do not move further business policy into HTTP or make unrelated gaps pilot prerequisites.
  comments:
    kind: directory
    disposition: migrate
    status: audited
    auditedAt: b2716ee0e80e8320025cf0d1f028125f3205d08a
    migrated: true
    migration:
      complexity: straightforward
      modelImpact: fits
      reason: >-
        The root only composes its API, controller, stats and mail collaborators during
        boot; it has no startup subscriptions or worker. Publish that graph atomically and
        update API/controller consumers, preserving member and email prerequisites. Make
        retention across repeated init explicit instead of the current unconditional
        replacement; notification persistence and delivery remain operation-level behavior.
      reviewedAt: f276513ba32b7114fd5cc4f7b02d3f82c0c31e3e
      sources:
        - core/server/services/comments/index.ts
        - core/server/services/comments/comments-service.js
        - core/server/services/comments/comments-service-emails.js
        - core/server/api/endpoints/comments.js
        - core/boot.js
      dependencies:
        complexity: straightforward
        reason: >-
          Models, settings, logging, mail and URL access already enter through the root. Remaining
          imports can be passed through the existing controller and email constructors: query
          helpers, event dispatch, translation, template I/O and HTML conversion. The email helper
          reads Email's renderer only when building a notification link, and Members supplies
          content-gating helpers without requiring a ready Members API; preserving those boundaries
          avoids introducing a startup cycle.
        preparation:
          - >-
            Wire controller/query helpers, comment-event dispatch and renderer dependencies from the
            root, retaining local template imports and using the shared clock's legacy Moment
            export.
          - >-
            Inject unsubscribe-link creation as a call-time capability rather than reading Email's
            renderer during Comments construction; preserve the independent content-gating
            dependency.
        reviewedAt: 44ad405bff4a8700af2ed51f833ebc08d25dddff
        sources:
          - core/server/services/comments/index.ts
          - core/server/services/comments/comments-controller.js
          - core/server/services/comments/comments-service.js
          - core/server/services/comments/comments-service-emails.js
          - core/server/services/comments/comments-service-email-renderer.js
          - core/server/services/members/api.js
          - core/server/services/members/content-gating.js
    entryPoints:
      - comments/index.ts
    notes: >-
      The migrated root and its contracts were re-audited at auditedAt, including the
      restricted template reader and call-time unsubscribe-link capability. The migration
      assessments retain their historical reviewedAt revisions. The prior wrapper is recorded at
      https://github.com/TryGhost/Ghost/blob/bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e/ghost/core/core/server/services/comments/index.js.
      Both recorded dependency preparations were applied in the migration. Account for the
      nested mail transport and external event subscribers. This background audit is separate
      from the eight-service pilot; recorded does not mean fully tested.
    contracts:
      exports:
        summary: >-
          The TypeScript root default-exports defineService; its service is one object holding
          the api and controller, read through the guarded getter after init. The API exposes
          comment/query/moderation/vote/report methods plus a synchronous enabled getter and
          synchronous access checks. Domain operations return promises. Controller request
          methods accept frames, return models, pages, count maps or no result, and set
          invalidation headers; destroy rejects rather than deleting. The HTTP adapter owns
          serialization/status codes. Units assert selected controller arguments, errors and
          headers with fake services. The real-root integration pins init resolving undefined
          and the published controller's service being the published api.
        sources:
          - core/server/services/comments/index.ts
          - core/kernel/define-service.ts
          - core/server/services/comments/comments-service.js
          - core/server/services/comments/comments-controller.js
          - core/server/services/comments/comments-stats-service.js
          - core/server/api/endpoints/comments-members.js
          - core/server/api/endpoints/utils/serializers/output/mappers/comments.js
        tests:
          - test/unit/server/services/comments/comments-controller.test.js
          - test/integration/services/comments-service.test.js
          - test/unit/kernel/define-service.test.ts
      construction:
        summary: >-
          The initializer constructs GhostMailer, CommentsService, CommentsStats and
          CommentsController once and returns them together; the service constructs its email
          helper, which constructs an isolated Handlebars renderer. The root injects models,
          settings/helper/URL facades, members.contentGating, DomainEvents, the
          MemberCommentEvent class, the byNQL query helper, i18n's t, a call-time
          unsubscribe-link capability that reads Email's renderer when a reply notification is
          sent, and a template reader limited to the service's own three email templates.
          Stats retains the DB module. No other production construction of these comments
          classes was found. Units instantiate deep classes with fakes; the DB integration
          invokes the real root and asserts pinned projections and that repeated init retains
          the api and controller, not transport ownership. Root units assert UTF-8 strings from
          all three templates and reject unknown, traversal and inherited-property names before
          any file I/O.
        sources:
          - core/server/services/comments/index.ts
          - core/server/services/comments/comments-service.js
          - core/server/services/comments/comments-service-emails.js
          - core/server/services/comments/comments-service-email-renderer.js
          - core/server/lib/mail/ghost-mailer.js
        tests:
          - test/unit/server/services/comments/index.test.ts
          - test/unit/server/services/comments/comments-service.test.js
          - test/unit/server/services/comments/comments-service-emails.test.js
          - test/integration/services/comments-service.test.js
      consumers:
        summary: >-
          Admin/member comment, reply, report and reaction endpoints retain the root and read
          its service's controller/api per call. Members BREAD similarly reads the service's api
          for bulk hiding; the Content post mapper reads the service's api.enabled through the
          guarded getter, which throws before init. Creation/replies dispatch MemberCommentEvent
          to members-events after notifications; comments registers no event subscriber. HTTP
          asserts selected CRUD/moderation, redaction, vote access, private-site guards, headers
          and bulk-hide results. Mail-intercepted comment/reply tests await test DomainEvents
          settlement and assert member timestamps; none pins production subscriber completion
          before the response. Mapper units pin the Content post comments flag for off and paid
          and that serialization rejects with IncorrectUsageError before init, which production
          cannot reach because the Content API serves only after boot.
        sources:
          - core/server/api/endpoints/comments.js
          - core/server/api/endpoints/comments-members.js
          - core/server/api/endpoints/comment-replies.js
          - core/server/api/endpoints/comment-reports.js
          - core/server/api/endpoints/comment-likes.js
          - core/server/api/endpoints/comment-dislikes.js
          - core/server/api/endpoints/utils/serializers/output/mappers/posts.js
          - core/server/services/members/api.js
          - core/server/services/members/members-api/services/member-bread-service.js
          - core/server/web/comments/routes.js
          - core/server/services/comments/comments-service.js
          - core/server/services/members-events/last-seen-at-updater.js
          - core/shared/events/member-comment-event.js
        tests:
          - test/e2e-api/admin/comments.test.js
          - test/e2e-api/admin/member-commenting.test.js
          - test/e2e-api/members-comments/comments.test.js
          - test/e2e-api/members-comments/post-access.test.js
          - test/unit/api/endpoints/utils/serializers/output/mapper.test.js
      startup:
        summary: >-
          Boot awaits comments.init in the service Promise.all array alongside members and
          email-service initialization. The factory requires members for content gating and
          the Email root for a call-time unsubscribe-link capability, so neither needs to be
          ready at construction. Querying needs models/DB; notification paths additionally need
          settings, URL helpers, email-address policy and initialized i18n, with reply
          unsubscribe links requiring the email-service renderer at send time. Init has no
          feature gate; reading the service before init throws IncorrectUsageError. HTTP
          exercises normal booted paths; the integration calls the root and asserts pinned
          projections; mapper units pin the pre-init rejection. Root units initialize while
          Email's renderer is unavailable and assert that initialization never reads it.
          Other prerequisite failures and partially initialized dependencies are not characterized.
        sources:
          - core/boot.js
          - core/server/services/comments/index.ts
          - core/kernel/define-service.ts
          - core/server/services/comments/comments-service-emails.js
          - core/server/services/members/service.js
          - core/server/services/email-service/email-service-wrapper.js
          - core/shared/i18n/index.js
          - core/server/lib/mail/ghost-mailer.js
        tests:
          - test/unit/server/services/comments/index.test.ts
          - test/integration/services/comments-service.test.js
          - test/e2e-api/members-comments/comments.test.js
          - test/unit/api/endpoints/utils/serializers/output/mapper.test.js
          - test/unit/kernel/define-service.test.ts
      lifetime:
        summary: >-
          One api, controller, stats, email helper, renderer and mailer are retained across
          repeated init; there is no reset/shutdown or transport disposal. Comment, vote and
          report state lives in shared tables. Stats accesses db.knex when querying;
          notification helpers call retained settings/URL facades and read the current
          email-service renderer through the injected capability. HTML templates are read
          through the injected reader and compiled each render; text modules are
          require-cached. The real-root integration pins that repeated init retains the api and
          controller and that the controller counts published comments through the wired stats;
          restart is untested. Root units replace Email's renderer between unsubscribe-link
          calls and assert that the next call uses the replacement.
        sources:
          - core/server/services/comments/index.ts
          - core/kernel/define-service.ts
          - core/server/services/comments/comments-stats-service.js
          - core/server/services/comments/comments-service-emails.js
          - core/server/services/comments/comments-service-email-renderer.js
          - core/server/data/schema/schema.js
        tests:
          - test/unit/server/services/comments/index.test.ts
          - test/integration/services/comments-service.test.js
          - test/unit/server/services/comments/comments-service-emails-renderer.test.ts
          - test/unit/kernel/define-service.test.ts
      configuration:
        summary: >-
          enabled reads comments_enabled each time and treats unknown values as off. Most member
          operations enforce it; site-wide Admin moderation, bulk status updates and counts do
          not. Mail content/sender/URLs call settings/helpers per send, but shared url-utils freezes
          site/subdirectory/admin URLs at import. The translator retained
          by the renderer forwards to current i18n. GhostMailer captures transport/options at
          construction, while address normalization occurs at send. HTTP tests stub settings
          and mutate test config to assert disabled/paid access and support-address output;
          renderer units assert selected English/German text. The real-root integration pins
          enabled following the setting on each access with unknown values as off. Email units
          pin the notification date as a calendar day in the site timezone setting, English
          month abbreviations, for the author, reply and report templates. None establishes
          production config hot reload or transport refresh from the immutable config tree.
        sources:
          - core/server/services/comments/comments-service.js
          - core/server/services/comments/comments-service-emails.js
          - core/server/services/comments/comments-service-email-renderer.js
          - core/server/services/comments/comments-stats-service.js
          - core/server/services/settings-helpers/settings-helpers.js
          - core/shared/i18n/index.js
          - core/server/lib/mail/ghost-mailer.js
          - core/shared/config/SCHEMA.md
          - core/shared/url-utils.ts
        tests:
          - test/e2e-api/members-comments/comments.test.js
          - test/unit/server/services/comments/comments-service-emails-renderer.test.ts
          - test/unit/server/services/comments/comments-service-emails.test.js
          - test/integration/services/comments-service.test.js
      resources:
        summary: >-
          The nested mailer owns a configured Nodemailer transport; comments has no close/drain
          hook, task queue or subscription. Notifications are awaited sequentially, with file
          reads per render; HTTP request draining is external. Models borrow the shared DB.
          Report notification reads post, member and owner concurrently, then renders/sends;
          units assert its template and mail payload, not the lookup timing. Votes join a supplied
          transaction or open one and lock the comment before replacing votes; create/reply/report
          do not establish a transaction spanning persistence and
          mail. Domain event subscribers run separately and test allSettled is not a production
          drain. Vote units assert arguments using fake transactions; HTTP mail replaces
          GhostMailer.sendMail, preserving normalization but bypassing transport/provider work.
          Email units pin the reply notification's recipient, injected unsubscribe link,
          translated subject and skip conditions, and the member and moderation timestamps as
          the current time. Root units assert call-time Email renderer lookup, the method
          receiver and forwarding of the member UUID and comments option. The intercepted
          reply e2e exercises the capability through the real Email root.
        sources:
          - core/server/services/comments/index.ts
          - core/server/services/comments/comments-service.js
          - core/server/services/comments/comments-service-emails.js
          - core/server/services/comments/comments-service-email-renderer.js
          - core/server/lib/mail/ghost-mailer.js
          - core/server/models/comment.js
          - core/server/services/members-events/last-seen-at-updater.js
          - core/server/ghost-server.ts
          - test/utils/e2e-framework-mock-manager.js
        tests:
          - test/unit/server/services/comments/index.test.ts
          - test/unit/server/services/comments/comments-service-emails.test.js
          - test/unit/server/services/comments/comments-service.test.js
          - test/e2e-api/members-comments/comments.test.js
      failures:
        summary: >-
          Construction errors reject init, leave the service unavailable and allow a later
          explicit init to retry; the factory acquires nothing that needs cleanup. Operation
          errors reject.
          A comment/reply is saved before mail, domain dispatch and the final read, so later
          failure can reject after persistence; mail failure prevents that domain dispatch and
          remaining recipients. Reports also persist before mail; a later duplicate request
          returns without retrying notification. Reaction controllers read after mutation before
          setting headers. HTTP asserts sequential duplicate-report suppression, access errors
          and successful mail/persistence; units assert notification recipient deduplication with
          fake email helpers, and that a failed author notification rejects commentOnPost after
          the comment is saved without the member comment event or the final read, while an
          internal context skips notifications and still dispatches. Post-write read failures,
          retry effects and concurrent vote/report outcomes remain source-only.
        sources:
          - core/server/services/comments/index.ts
          - core/server/services/comments/comments-service.js
          - core/server/services/comments/comments-controller.js
          - core/server/services/comments/comments-service-emails.js
          - core/server/lib/mail/ghost-mailer.js
        tests:
          - test/unit/server/services/comments/comments-service.test.js
          - test/e2e-api/members-comments/comments.test.js
          - test/e2e-api/members-comments/post-access.test.js
    unverified: []
    blockers:
      - >-
        The real-root integration pins init resolving undefined, the published controller bound
        to the published api and repeated init retaining both; mapper units pin the Content post
        mapper rejecting before init. Failed construction and in-flight calls across a retried
        init remain uncharacterized beyond the kernel's generic tests.
      - >-
        Units now pin that a failed author notification rejects comment creation after
        persistence without the member comment event or final read. Reply/report notification
        failure, final read failure, recipient continuation, request retries and omitted
        invalidation headers remain uncharacterized; intercepted successful mail and fake
        recipient helpers do not establish notification durability.
      - >-
        Establish mail-transport ownership on replacement/shutdown and behavior of pending
        notification/domain-event work. Comments closes no transport and registers no cleanup;
        test DomainEvents settlement and HTTP response assertions are not production drain
        guarantees. Preserve the email renderer/i18n/email-address prerequisites.
      - >-
        Add real transaction failure/concurrency evidence for votes, report deduplication and
        moderation races. Vote units use fake transaction callbacks; sequential report tests
        do not establish atomic deduplication. Keep access/visibility, counts, consumer headers
        and bulk-hide behavior covered when the root interface changes.
  content-import:
    kind: directory
    disposition: migrate
    status: audited
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: straightforward
      modelImpact: fits
      reason: >-
        Already retains one boot-created importer and rejects use before init. Move that
        composition behind the helper and adapt the request/job forwarding methods while
        keeping validation at the boundary. Jobs and media are deliberately looked up when
        work runs; per-import stores, staging and cleanup remain with the importer and do
        not require a new root lifecycle.
      reviewedAt: f276513ba32b7114fd5cc4f7b02d3f82c0c31e3e
      sources:
        - core/server/services/content-import/index.ts
        - core/server/services/content-import/import/importer.ts
        - core/server/services/jobs-service/register-job-handlers.ts
        - core/boot.js
        - test/unit/server/services/content-import/index.test.ts
      dependencies:
        complexity: complex
        reason: >-
          The root injects the importer collaborators, but ZIP preparation still constructs archive
          and storage-backed asset collaborators below it. CSV, HTML and row-validation helpers also
          import libraries directly. Moving those dependencies requires preserving per-import
          acquisition and the existing call-time jobs, media, storage, URL and timezone lookups
          rather than capturing them at boot.
        preparation:
          - >-
            Make source and asset preparation accept root-wired factories, retaining new archive and
            asset-importer state per import and the existing storage acquisition timing.
          - >-
            Expose parser, filesystem, HTML, string and validation operations through local helper
            factories; preserve the row schemas and validate any proposed stateless-library
            allowance individually.
          - >-
            Use the existing clock compatibility export for Moment and inject logging without
            replacing the existing lazy jobs/media/storage callbacks.
        reviewedAt: 44ad405bff4a8700af2ed51f833ebc08d25dddff
        sources:
          - core/server/services/content-import/index.ts
          - core/server/services/content-import/import/source.ts
          - core/server/services/content-import/import/assets.ts
          - core/server/services/content-import/import/row.ts
          - core/server/services/content-import/import/staged-file.ts
          - core/server/data/importer/content-files.ts
    entryPoints:
      - content-import/index.ts
    notes: >-
      Preserve boot-created importer state, request validation, job dispatch, staging and
      completion reporting. Keep this background audit separate from the eight-service pilot.
    contracts:
      exports:
        summary: >-
          The TypeScript root exports synchronous init and Promise-returning importCSV,
          handleJob and allSettled functions. importCSV and handleJob throw synchronously
          before initialization; request-schema errors also throw synchronously, whereas
          accepted import preparation and handler failures reject promises. allSettled before
          init resolves. The deep ContentCSVImportJob class carries serializable upload,
          mapping, recipient and run metadata; it is a job definition, not another root.
          Units assert the pre-init handler/drain path and injected job serialization. A
          separately imported root in HTTP tests checks pre-init import, two successful init
          calls, schema errors and asynchronous unreadable-file rejection. Root units also
          assert pre-init request/job errors, harmless pre-init draining, validation before
          forwarding and retained request/job/drain receivers.
        sources:
          - core/server/services/content-import/index.ts
          - core/server/services/content-import/import/schema.ts
          - core/server/services/content-import/jobs/content-csv-import-job.ts
        tests:
          - test/unit/server/services/content-import/index.test.ts
          - test/unit/server/services/content-import/import/importer.test.ts
          - test/e2e-api/admin/posts-importer.test.js
      construction:
        summary: >-
          First init constructs one private ContentCSVImporter, ImportRunStore,
          BookshelfPostsRepository with its relation resolver, and GhostMailer; successful
          later calls retain that graph. The repository retains models. Converter factories
          and the current media-inliner implementation are resolved per job, with a new
          PostMediaInliner/cache for that job. Dispatch resolves the current jobs instance;
          staging and removal each resolve the imports storage adapter. Class tests assert
          per-run converter/media construction and injected model transaction arguments;
          filesystem tests exercise the stager with LocalStorageBase. Root units now pin one
          importer across repeated init, no publication after failed mailer construction and
          successful retry. Late jobs/media/storage lookups use controlled adapters with a
          real CSV and importer. Partial resource-acquisition cleanup remains source-only.
        sources:
          - core/server/services/content-import/index.ts
          - core/server/services/content-import/import/importer.ts
          - core/server/services/content-import/import/post-repository.ts
          - core/server/services/content-import/import/relations.ts
          - core/server/services/content-import/import/staged-file.ts
          - core/server/lib/mail/ghost-mailer.js
        tests:
          - test/unit/server/services/content-import/import/importer.test.ts
          - test/unit/server/services/content-import/import/post-repository.test.ts
          - test/unit/server/services/content-import/import/staged-file.test.ts
          - test/unit/server/services/content-import/index.test.ts
      consumers:
        summary: >-
          Boot owns init. The Admin posts upload endpoint retains the root, forwards upload
          metadata/mapping and the current user's email, and returns HTTP 202 with import_id
          and total after staging, preflight and dispatch. Authentication, feature flag,
          upload validation and content-import permission belong to HTTP/API layers. Jobs
          registration awaits root handleJob for content-csv-import; the separate
          site-content-import job is outside this root. Production shutdown does not call
          its test-facing allSettled. Real booted HTTP tests assert role/flag gates, report
          mail through a mocked transport, persisted posts/relations and intercepted remote
          media. The handler unit only reaches the real root's pre-init error. Webhook tests
          include a normal-publish control, then assert two imported rows including an
          update, two completion emails, no Email rows and bounded webhook silence.
        sources:
          - core/boot.js
          - core/server/api/endpoints/posts.js
          - core/server/web/api/endpoints/admin/routes.js
          - core/server/services/jobs-service/register-job-handlers.ts
          - core/server/services/content-import/index.ts
        tests:
          - test/e2e-api/admin/posts-importer.test.js
          - test/e2e-webhooks/posts-importer.test.ts
          - test/unit/server/services/jobs-service/register-job-handlers.test.ts
          - test/unit/server/services/content-import/index.test.ts
      startup:
        summary: >-
          Boot invokes synchronous init in required services after DB setup and the call to
          media-inliner.init, before handler registration, jobs start and HTTP readiness.
          Construction alone does not resolve the jobs instance or media implementation;
          dispatch and processing require them later. The request needs owner lookup when
          no caller email exists, readable local staging and settings for batch tags; jobs
          additionally need converters, models/storage and optional URL lookup. Real HTTP
          tests establish selected normal boot-to-job outcomes, while root/handler units
          establish early errors. Root units additionally assert late dependency acquisition
          and failed-construction retry; held boot readiness and same-process boot contracts
          remain outside their coverage.
        sources:
          - core/boot.js
          - core/server/services/content-import/index.ts
          - core/server/services/content-import/import/importer.ts
          - core/server/services/jobs-service/register-job-handlers.ts
        tests:
          - test/unit/server/services/content-import/index.test.ts
          - test/unit/server/services/jobs-service/register-job-handlers.test.ts
          - test/e2e-api/admin/posts-importer.test.js
      lifetime:
        summary: >-
          There is no reset, stop, abort or public implementation accessor. The module retains
          its graph across init calls; run records are mutable, in memory and normally
          released after completion email. finish/fail alone do not settle waiters; release
          resolves them when the map becomes empty. Lazy age/count eviction excludes running
          runs. Media promises, including failed results, are cached by exact URL within one
          job, not across jobs. Store units assert retention/eviction and two-run release
          waiting; media units hold an import to prove sharing, and assert separate-instance
          isolation. Root units retain the same request/job/drain receiver and call handleJob
          directly after repeated init. Dispatch is stubbed; the jobs backend queue is not
          exercised. Duplicate/concurrent job handling and late admissions during shutdown
          remain source-only. The importer drain test awaits handle before
          checking settlement, so it cannot establish the final cleanup boundary.
        sources:
          - core/server/services/content-import/index.ts
          - core/server/services/content-import/import/store.ts
          - core/server/services/content-import/import/media.ts
          - core/server/services/content-import/import/importer.ts
        tests:
          - test/unit/server/services/content-import/import/store.test.ts
          - test/unit/server/services/content-import/import/media.test.ts
          - test/unit/server/services/content-import/import/importer.test.ts
          - test/unit/server/services/content-import/index.test.ts
      configuration:
        summary: >-
          Each request captures recipient, timezone-derived batch tags and mapping into its
          job; each execution reparses the staged source and rechecks the fixed 100-row cap.
          Timezone fallback validates nonempty string shape, not a valid timezone name.
          Local-media classification reads current config accessors for site/subdir/CDN URLs;
          this does not imply production hot reload of the immutable config tree. Admin URLs
          come through the shared frozen URL helper; published URLs query the retained URL
          service per result. Tests assert injected timezone tags, mapping forwarding, cap
          boundaries, local URL classification and real HTTP newer-timestamp/duplicate
          handling. They do not pin retained-root config capture/replacement. CSV defaults
          are published/public posts; explicit supported fields and supplied HTML/Markdown
          are validated/converted before writes, rather than inheriting site visibility.
        sources:
          - core/server/services/content-import/index.ts
          - core/server/services/content-import/import/importer.ts
          - core/server/services/content-import/import/post-data.ts
          - core/server/services/content-import/import/local-media-url.ts
          - core/server/services/content-import/import/post-link.ts
          - core/shared/url-utils.ts
          - core/shared/config/index.ts
        tests:
          - test/unit/server/services/content-import/import/importer.test.ts
          - test/unit/server/services/content-import/import/media.test.ts
          - test/e2e-api/admin/posts-importer.test.js
      resources:
        summary: >-
          The root retains a mailer and run store, borrows jobs/models/storage and owns staged
          uploads, extracted directories and per-job media caches. It registers no timer or
          event listener itself. Parsing buffers the complete CSV before applying the row
          cap; archive extraction uses the shared zip helper without explicit size limits.
          Asset groups store in parallel with rollback on storage failure; successfully
          stored assets/media are not compensated for later row or mail failures. Posts use
          individual DB transactions, not one import transaction. Handler finally awaits
          extracted-source cleanup and best-effort mail, releases the run, then awaits staged
          deletion: allSettled can finish before that last deletion. Units and filesystem
          integration assert selected cleanup, asset rollback and cache behavior; real HTTP
          tests assert file removal under injected storage failures and contributor/tag DB
          rollback under a rejected post write. They do not prove root/job shutdown drains
          mail, staged cleanup, providers or outstanding DB work.
        sources:
          - core/server/services/content-import/index.ts
          - core/server/services/content-import/import/importer.ts
          - core/server/services/content-import/import/assets.ts
          - core/server/services/content-import/import/source.ts
          - core/server/services/content-import/import/post-repository.ts
          - core/server/services/content-import/csv/parse.ts
          - core/server/data/importer/import-archive.ts
          - core/server/data/importer/content-files.ts
          - core/server/lib/mail/ghost-mailer.js
          - package.json
        tests:
          - test/unit/server/services/content-import/import/staged-file.test.ts
          - test/unit/server/services/content-import/import/assets.test.ts
          - test/integration/importer/content-import-source.test.ts
          - test/e2e-api/admin/posts-importer.test.js
      failures:
        summary: >-
          Owner lookup fails before staging; invalid/staging/parser/cap failures reject before
          dispatch, and dispatch failure releases the run with best-effort staged cleanup.
          Expected row validation, media and DB write failures are recorded and later rows
          continue. All attempted writes failing reports an aggregate but still marks the
          run complete; unexpected preparation/converter/media failures reject the handler.
          URL failures retain successful writes without links. Completion mail and returned
          source/staged cleanup errors are reported without rejecting successful work; the
          root reporter catches logging/reporting failures. Source preparation's own cleanup
          failure can mask its initial error. Injected importer tests assert these selected
          channels; fake repository transaction tests prove options/rejection, not rollback.
          Real HTTP proves narrow relation rollback and partial-asset failure outcomes, not
          concurrent deduplication or rollback of an already-written update's later timestamp.
        sources:
          - core/server/services/content-import/index.ts
          - core/server/services/content-import/import/importer.ts
          - core/server/services/content-import/import/source.ts
          - core/server/services/content-import/import/assets.ts
          - core/server/services/content-import/import/post-repository.ts
          - core/server/services/content-import/import/completion-email.ts
        tests:
          - test/unit/server/services/content-import/import/importer.test.ts
          - test/unit/server/services/content-import/import/post-repository.test.ts
          - test/e2e-api/admin/posts-importer.test.js
          - test/unit/server/services/content-import/index.test.ts
    unverified: []
    blockers:
      - >-
        Root units now pin retained importer identity, failed construction/retry and late
        jobs/media/storage resolution; HTTP tests cover normal boot availability. Keep those
        contracts when changing composition. Retention across actual same-process boot and
        partially acquired resource cleanup still need separate characterization.
      - >-
        Characterize the release-before-staged-deletion drain boundary with a held cleanup,
        then define root/job ownership for admission, cancellation, mail and filesystem drain.
        Existing allSettled coverage does not prove complete handler settlement or restart.
      - >-
        Preserve staged-file ownership when storage changes between stage/remove; cover
        cleanup failure masking, orphan recovery and bounded CSV/archive resources. The row
        cap is applied after full reads/extraction and does not itself bound bytes or memory.
      - >-
        Pin duplicate/concurrent jobs and concurrent source-ID/slug writes, partial import
        persistence and second timestamp-write rollback if these paths change. Existing real
        relation rollback and sequential duplicate tests do not establish those contracts.
      - >-
        Add focused retained-root settings/config coverage before changing those boundaries.
        Root units now hold dispatch to prove request acceptance waits, then process the
        dispatched job through the real retained importer. Broader import policy, validation and pre-existing storage
        gaps belong to this service's later work and do not gate the eight-service pilot.
  custom-redirects:
    kind: directory
    disposition: migrate
    status: audited
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: complex
      modelImpact: fits
      reason: >-
        Each init rebuilds the redirect graph while mounted middleware retains its manager,
        so lifetime and consumer identity need an explicit decision. A retained manager
        already supports activate() for in-place reload; preserve that operation and the
        difference between tolerant boot loading and strict uploads. This is coordinated
        wiring and failure-policy work, with no demonstrated need to extend the helper.
      reviewedAt: f276513ba32b7114fd5cc4f7b02d3f82c0c31e3e
      sources:
        - core/server/services/custom-redirects/index.js
        - core/server/services/custom-redirects/redirects-service.ts
        - core/frontend/web/site.js
        - core/server/api/endpoints/redirects.js
        - core/boot.js
      dependencies:
        complexity: complex
        reason: >-
          Store, live manager and dry-run manager are already injected, but parser and backup-path
          helpers are also imported directly by storage adapters and API endpoints. Those shared
          helpers currently load YAML, path and Moment themselves, so routing all of their
          dependencies through the service root would couple adapter construction back to its owner.
        preparation:
          - >-
            Give redirect parsing/serialization and backup-path support a shared library home used
            by adapters and endpoints; keep those operations independent of the running service.
          - >-
            Require the existing dry-run-manager injection instead of its implementation fallback,
            and inject logging, validation and error normalization from the root while retaining the
            mounted manager.
        reviewedAt: 44ad405bff4a8700af2ed51f833ebc08d25dddff
        sources:
          - core/server/services/custom-redirects/index.js
          - core/server/services/custom-redirects/redirects-service.ts
          - core/server/services/custom-redirects/redirect-config-parser.ts
          - core/server/services/custom-redirects/utils.ts
          - core/server/adapters/redirects/FileStore.ts
          - core/server/adapters/redirects/S3RedirectsStore.ts
          - core/server/api/endpoints/redirects.js
    entryPoints:
      - custom-redirects/index.js
    notes: >-
      Preserve the stable mounted redirect handler across uploads, boot's tolerant activation
      and stricter replacement validation. Repeated root initialization replaces the graph;
      persistence, activation and export are separate boundaries.
    contracts:
      exports:
        summary: >-
          CommonJS exports one object with receiver-independent init plus api/middleware getters.
          api is undefined before init; middleware throws before a manager exists. init returns
          the new service's initialization promise. The deep class has receiver-dependent async
          init, activate, replace and getAll; only getAll returns stored configuration. Parser,
          validation and backup helpers are also deep imports. Tests exercise deep service/parser
          methods and HTTP consumers, not root descriptors, pre-init access or detached API calls.
        sources:
          - core/server/services/custom-redirects/index.js
          - core/server/services/custom-redirects/redirects-service.ts
          - core/server/services/custom-redirects/redirect-config-parser.ts
          - core/server/services/custom-redirects/validation.js
          - core/server/services/custom-redirects/utils.ts
        tests:
          - test/unit/server/services/custom-redirects/redirects-service.test.ts
          - test/unit/server/services/custom-redirects/redirect-config-parser.test.ts
      construction:
        summary: >-
          Import loads dependencies without creating the manager/service. Every init replaces
          the private manager first, resolves the adapter-manager-cached redirects store, then
          publishes a new service before its asynchronous activation finishes. No guard or
          shared pending promise exists. The service retains store, manager, validator and a
          dry-run-manager factory. Adapter failure can leave a new empty manager alongside an
          older API. Boot is the sole runtime root init caller found; construction/publication
          and partial graph replacement are source-only.
        sources:
          - core/server/services/custom-redirects/index.js
          - core/server/services/custom-redirects/redirects-service.ts
          - core/server/services/adapter-manager/index.ts
          - core/kernel/adapter-manager/adapter-manager.ts
          - core/boot.js
        tests: []
      consumers:
        summary: >-
          Frontend mounting retains the current manager's bound handler after offers and tracked
          links, before static/dynamic content. Redirect upload/download and site export retain
          the root and read api per operation; file/S3 stores deep-import parsers and backup naming.
          Legacy HTTP proves two uploads update the mounted handler, disk data and backups. Admin
          HTTP proves YAML upload/download content, while site-export HTTP proves redirect-file
          presence and reupload success, not nonempty redirect equality or frontend precedence.
          Exporter failure units inject the export function, not this root.
        sources:
          - core/frontend/web/site.js
          - core/server/api/endpoints/redirects.js
          - core/server/api/endpoints/exports.js
          - core/server/data/exporter/site-exporter.ts
          - core/server/adapters/redirects/FileStore.ts
          - core/server/adapters/redirects/S3RedirectsStore.ts
        tests:
          - test/legacy/api/admin/redirects.test.js
          - test/e2e-api/admin/redirects.test.js
          - test/e2e-api/admin/exports-download.test.js
          - test/unit/server/data/exporter/site-exporter.test.ts
      startup:
        summary: >-
          Boot awaits custom redirects after route settings and before link redirection, themes,
          Express mounting and readiness, even with frontend disabled. Init reads the store before
          clearing/loading the new manager. The deep init catches/logs activation errors so unreadable
          storage normally allows boot with an empty new graph; adapter/construction failures
          occur outside that catch. API/middleware are already published while loading. Deep tests
          pin swallowed store failure and no clear on failed read, not pending root readiness,
          backend-only composition or early API calls.
        sources:
          - core/boot.js
          - core/server/services/custom-redirects/index.js
          - core/server/services/custom-redirects/redirects-service.ts
          - core/frontend/web/site.js
        tests:
          - test/unit/server/services/custom-redirects/redirects-service.test.ts
          - test/legacy/api/admin/redirects.test.js
      lifetime:
        summary: >-
          activate and replace mutate the retained manager, preserving its mounted handler within
          one graph. Re-init replaces the private graph while old mounted handlers keep their old
          manager. There is no watcher, event subscription, cross-instance notification or operation
          serialization. activate rereads storage; replace validates/dry-runs, awaits persistence,
          then loads its original array without rereading or cloning it. Concurrent operations or
          caller mutation during the await can diverge storage and active routes. The deep identity
          test only compares an injected stub method; real legacy uploads prove same-boot activation,
          not re-init, retained old APIs or concurrent consistency.
        sources:
          - core/server/services/custom-redirects/index.js
          - core/server/services/custom-redirects/redirects-service.ts
          - core/server/web/shared/dynamic-redirect-manager.js
          - core/frontend/web/site.js
        tests:
          - test/unit/server/services/custom-redirects/redirects-service.test.ts
          - test/legacy/api/admin/redirects.test.js
      configuration:
        summary: >-
          Each manager captures permanent max-age; the dry-run factory reads config when invoked.
          The subdirectory callback uses shared URL helpers, whose production URL values are frozen.
          Store selection/options are adapter-owned. FileStore prefers YAML over JSON, returns
          an empty list if neither exists and always writes JSON. S3 reads/writes JSON and treats
          NotFound/NoSuchKey as an empty list. Validation requires nonempty from/to and a
          compilable regex, without requiring boolean permanent. YAML parsing loads 302 before 301;
          serialization emits 301 before 302, retaining order within each group. Manager matching
          uses insertion order, optional case-insensitive regexes and trailing slashes; target query
          values override incoming ones and temporary redirects use max-age zero. Parser/manager
          tests pin selected encoding/status/order behavior; mixed overlaps and duplicate paths
          through full upload/download/activation remain uncovered.
        sources:
          - core/server/services/custom-redirects/index.js
          - core/server/services/custom-redirects/redirect-config-parser.ts
          - core/server/services/custom-redirects/validation.js
          - core/server/web/shared/dynamic-redirect-manager.js
          - core/shared/url-utils.ts
          - core/shared/config/validated.ts
          - core/kernel/adapter-manager/adapter-manager.ts
          - core/server/adapters/redirects/FileStore.ts
          - core/server/adapters/redirects/S3RedirectsStore.ts
        tests:
          - test/unit/server/services/custom-redirects/redirect-config-parser.test.ts
          - test/unit/server/services/custom-redirects/validation.test.ts
          - test/unit/server/web/shared/dynamic-redirect-manager.test.js
          - test/e2e-api/admin/redirects.test.js
      resources:
        summary: >-
          Each graph owns its manager/router and temporary dry-run managers, borrowing the cached
          redirects adapter; the root owns no timer, watcher, DB transaction or disposer. FileStore
          owns filesystem writes/backups; S3 store owns its SDK client and remote backup/write calls.
          Neither root nor stores expose pending-operation drain or client teardown. File and
          S3 backup names have second precision and can overwrite in that interval. Local store tests use
          temporary files; S3 integration requires GHOST_TEST_S3_AVAILABLE=1 and covers storage,
          not root lifecycle; its multi-backup test spaces writes apart. No shutdown or
          multi-instance update guarantee is inferred.
        sources:
          - core/server/services/custom-redirects/index.js
          - core/server/services/custom-redirects/redirects-service.ts
          - core/server/web/shared/dynamic-redirect-manager.js
          - core/server/adapters/redirects/FileStore.ts
          - core/server/adapters/redirects/S3RedirectsStore.ts
          - core/server/services/custom-redirects/utils.ts
          - core/server/ghost-server.ts
        tests:
          - test/unit/server/adapters/redirects/file-store.test.ts
          - test/integration/adapters/redirects/s3-redirects-store.test.ts
      failures:
        summary: >-
          activate preserves existing routes when reading fails, then clears before per-item
          validation/loading, skips/logs invalid entries and reports when none load. replace
          rejects batch/dry-run errors before writing; after persistence, live-manager failure
          rejects with partial activation and no rollback. Init swallows activation errors unless
          error handling itself throws. Units pin these stages using fake managers/in-memory storage.
          File tests prove selected backup failure recovery, but the JSON atomic-write failure fake
          fails the first backup write, not a later canonical move. HTTP success tests do not prove
          these fault paths. Site export can omit failed redirects while retaining other files.
        sources:
          - core/server/services/custom-redirects/index.js
          - core/server/services/custom-redirects/redirects-service.ts
          - core/server/services/custom-redirects/redirect-config-parser.ts
          - core/server/adapters/redirects/FileStore.ts
          - core/server/adapters/redirects/S3RedirectsStore.ts
          - core/server/data/exporter/site-exporter.ts
          - core/server/api/endpoints/redirects.js
        tests:
          - test/unit/server/services/custom-redirects/redirects-service.test.ts
          - test/unit/server/adapters/redirects/file-store.test.ts
          - test/unit/server/data/exporter/site-exporter.test.ts
          - test/unit/server/services/custom-redirects/redirect-config-parser.test.ts
    unverified: []
    blockers:
      - >-
        Pin root export/pre-init behavior, pending and failed initialization, backend-only boot
        and repeated/concurrent graph replacement. Real uploads establish one manager's activation,
        not replacement of a mounted handler; preserve tolerant store failures separately from
        construction errors that still reject boot.
      - >-
        Cover concurrent activate/replace and caller mutation while persistence is pending, with
        actual store/manager composition. Define ordering and cross-instance refresh before adding
        lifecycle ownership; there is no operation queue, watcher or notifier today.
      - >-
        Extend storage failure coverage to canonical writes/moves after successful backup, failed
        cleanup and remote partial/ambiguous writes. A rejected write can leave temporary files,
        and post-write activation failure can leave persisted config with only partial live routes.
        Existing injected store/manager tests do not establish end-to-end rollback or recovery.
      - >-
        Add real HTTP contracts for overlapping/duplicate redirects, status-group round trips,
        frontend precedence, regex captures and configured subdirectories. The manager's subdirectory
        unit uses an identity callback and preserves a prefix already in req.url, rather than
        proving root callback wiring. Preserve nonempty redirect values through site export/reimport.
      - >-
        Define ownership of borrowed adapters and pending writes before shutdown/re-init; no root
        drain or S3-client disposal exists. Conditional storage integration coverage is not proof
        of wrapper behavior or safe teardown.
  custom-theme-settings:
    kind: directory
    disposition: migrate
    status: audited
    migrated: true
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: straightforward
      modelImpact: fits
      reason: >-
        Construction only wires a model and shared cache; theme activation is already a
        separate awaited operation used both at boot and after uploads. Return one retained
        implementation, await its initializer before theme startup, and update api
        consumers. Preserve activation on every boot and theme change rather than moving
        activation into the one-time factory; current activation races are separate
        behavioral gaps.
      reviewedAt: f276513ba32b7114fd5cc4f7b02d3f82c0c31e3e
      sources:
        - core/server/services/custom-theme-settings/index.ts
        - core/server/services/custom-theme-settings/custom-theme-settings-service.js
        - core/server/services/themes/activation-bridge.js
        - core/server/services/themes/storage.js
        - core/boot.js
      dependencies:
        complexity: straightforward
        reason: >-
          The model and shared render cache already arrive through the constructor. Remaining
          non-allowlisted imports are Lodash, NQL and debug in one implementation; these can be
          supplied by the root without changing theme activation ownership or cache identity.
        preparation:
          - >-
            Inject debug, NQL evaluation and the remaining utility operations into the existing
            constructor; keep theme activation separate from construction.
        reviewedAt: 44ad405bff4a8700af2ed51f833ebc08d25dddff
        sources:
          - core/server/services/custom-theme-settings/index.ts
          - core/server/services/custom-theme-settings/custom-theme-settings-service.js
          - core/server/services/custom-theme-settings/custom-theme-settings-bread-service.js
          - core/server/services/themes/activation-bridge.js
    entryPoints:
      - custom-theme-settings/index.ts
    notes: >-
      Initializer adoption was rechecked against 2196fa4e4cf4ef4ecf41df16cfe9f460270cadf5;
      auditedAt and the migration assessments retain their historical provenance. The prior
      wrapper is recorded at https://github.com/TryGhost/Ghost/blob/bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e/ghost/core/core/server/services/custom-theme-settings.js.
      Boot now
      awaits one retained implementation before separate theme activation on every boot.
      The root injects the model and shared render cache. The earlier dependency proposal to
      inject utility operations is superseded: lodash/isEmpty, the NQL public entry and
      namespaced debug tracing remain direct library imports under boundary allowances.
      Root/implementation/cache/model/middleware units and real custom-settings/theme API
      checks provide migration validation; concurrency, partial writes and initial visibility
      remain separate behavioral gaps.
    contracts:
      exports:
        summary: >-
          The TypeScript root default-exports defineService with async init and a guarded
          service getter exposing activateTheme, listSettings, updateSettings and
          copySettingsBetweenThemes plus mutable working state. CommonJS consumers use
          default at the boundary and read service at operation time. There are no extras,
          reset, stop or drain exports. The implementation and BREAD classes remain deep
          exports under services/custom-theme-settings; the shared value-cache class remains under
          shared/custom-theme-settings-cache.
        sources:
          - core/server/services/custom-theme-settings/index.ts
          - core/server/services/custom-theme-settings/custom-theme-settings-service.js
          - core/server/services/custom-theme-settings/custom-theme-settings-bread-service.js
          - core/shared/custom-theme-settings-cache/custom-theme-settings-cache.js
          - core/kernel/define-service.ts
        tests:
          - test/unit/server/services/custom-theme-settings/index.test.ts
          - test/unit/kernel/define-service.test.ts
      construction:
        summary: >-
          Import declares the initializer and loads its implementation and namespaced debug tracing
          function. Its factory resolves the shared model registry/cache, then constructs one
          implementation and BREAD repository. Successful init retains that implementation;
          construction performs no setting query or activation and acquires no resources.
          No alternate runtime implementation construction was found. Unit tests inject a fake
          model/cache; API tests explicitly init and activate.
          Root units pin that init performs no setting query, frontend activation or shared
          cache change, and that the real endpoint then lists no settings.
        sources:
          - core/server/services/custom-theme-settings/index.ts
          - core/server/services/custom-theme-settings/custom-theme-settings-service.js
          - core/shared/custom-theme-settings-cache/index.js
        tests:
          - test/e2e-api/admin/custom-theme-settings.test.js
          - test/unit/server/services/custom-theme-settings/index.test.ts
      consumers:
        summary: >-
          Boot awaits the initializer. Theme activation for boot/API/override awaits
          service.activateTheme before the frontend bridge; theme upload copies settings through
          service. The custom settings endpoint lists/updates through the retained initializer's
          service getter without capturing the implementation at import. The data
          importer indirectly triggers activation without awaiting it. Global template options
          and local preview keys independently read the shared cache. No other runtime root
          or deep-constructor caller was found. HTTP cases cover edits and successful theme-copy
          activation; middleware tests replace the cache reader, leaving the complete import
          and rendering paths untested. Root units hold the settings read in each of the real
          bridge's boot, API and override methods to show frontend activation waits for it,
          then read the result through the real endpoint.
        sources:
          - core/boot.js
          - core/server/services/themes/activation-bridge.js
          - core/server/services/themes/storage.js
          - core/server/api/endpoints/custom-theme-settings.js
          - core/server/data/importer/importers/data/custom-theme-settings-importer.js
          - core/frontend/services/theme-engine/middleware/update-global-template-options.js
          - core/frontend/services/theme-engine/middleware/update-local-template-options.js
        tests:
          - test/e2e-api/admin/custom-theme-settings.test.js
          - test/e2e-api/admin/themes.test.js
          - test/unit/frontend/services/theme-engine/middleware.test.js
          - test/unit/server/services/custom-theme-settings/index.test.ts
      startup:
        summary: >-
          Boot awaits init after database/settings setup and before awaited themes.init;
          initServicesForFrontend also runs on backend-only boots. Theme activation populates
          both caches; initial construction leaves listSettings empty and the shared cache
          unchanged. Reading service before init throws through defineService. Boot theme
          loading catches/logs activation failure and continues, so readiness
          alone does not establish successful custom-setting activation. API tests manually
          activate a fixture definition. A boot-harness unit runs initServicesForFrontend with
          the real root, themes service and bridge: init precedes the settings read, frontend
          activation and offers, a second boot initializes and activates again, and a failed
          settings lookup is logged while startup continues without activation or cache changes.
          Holding root initialization pending blocks theme startup, offers and frontend-phase
          completion; an initializer rejection propagates unchanged without starting themes.
        sources:
          - core/boot.js
          - core/server/services/custom-theme-settings/index.ts
          - core/server/services/themes/index.js
          - core/server/services/themes/activate.js
          - core/server/services/custom-theme-settings/custom-theme-settings-service.js
        tests:
          - test/e2e-api/admin/custom-theme-settings.test.js
          - test/unit/server/services/custom-theme-settings/service.test.js
          - test/unit/server/services/custom-theme-settings/index.test.ts
      lifetime:
        summary: >-
          Successful init retains one implementation and the borrowed shared value cache;
          repeated init leaves active settings and pending operations on that same instance.
          This replaces the historical wrapper's api replacement behavior. Activation tracks
          one promise, skips matching concurrent name/settings and waits for a prior activation,
          but multiple differing
          waiters can then start together. Updates are not serialized with activation or one
          another. listSettings returns shallow copies; nested definition objects remain
          shared. Cache getAll is also shallow, and no stop clears or drains this state. Root
          units hold one activation across repeated init; it completes on the retained
          implementation, and later activation through the bridge remains usable. Retained
          identity itself is covered by the kernel's shared initializer tests rather than a
          new feature-specific assertion.
        sources:
          - core/server/services/custom-theme-settings/index.ts
          - core/server/services/custom-theme-settings/custom-theme-settings-service.js
          - core/shared/custom-theme-settings-cache/custom-theme-settings-cache.js
        tests:
          - test/unit/server/services/custom-theme-settings/service.test.js
          - test/unit/shared/custom-theme-settings-cache/cache.test.ts
          - test/unit/server/services/custom-theme-settings/index.test.ts
          - test/unit/kernel/define-service.test.ts
      configuration:
        summary: >-
          Activation reconciles current theme definitions with stored values: remove missing
          or changed types, reset invalid selects and add defaults. Definitions are shallowly
          copied for editing; updates accept only keys/values and validate select, boolean and
          six-digit color values. Public-cache visibility is evaluated on updates, but initial
          activation passes DB rows without visibility metadata. Model reads/writes normalize
          booleans and image URLs. The root reads no config; theme choice is supplied by
          callers and model URL conversion uses shared URL utilities.
        sources:
          - core/server/services/custom-theme-settings/custom-theme-settings-service.js
          - core/server/models/custom-theme-setting.js
          - core/server/data/schema/schema.js
          - core/server/services/themes/index.js
        tests:
          - test/unit/server/services/custom-theme-settings/service.test.js
          - test/unit/server/models/custom-theme-setting.test.js
          - test/legacy/models/base/overrides.test.js
      resources:
        summary: >-
          The implementation owns an internal settings map and tracks activation work while
          borrowing the process cache and model/database. It creates no timer, subscription,
          worker or dedicated connection. Copy uses a model transaction for the destination
          check and inserts, with source rows read beforehand. Activation/update writes have
          no encompassing transaction, and neither boot nor initializer registers cancellation
          or pending-operation cleanup.
        sources:
          - core/server/services/custom-theme-settings/custom-theme-settings-service.js
          - core/server/services/custom-theme-settings/custom-theme-settings-bread-service.js
          - core/shared/custom-theme-settings-cache/index.js
          - core/boot.js
          - core/server/ghost-server.ts
        tests:
          - test/unit/server/services/custom-theme-settings/service.test.js
      failures:
        summary: >-
          Unknown keys and invalid select/boolean/color values reject before writes; HTTP tests
          pin 422 for unknown/select values. Persistence and malformed-definition failures
          propagate. Activation assigns activeThemeName before syncing and clears its promise
          in finally without rolling back rows/caches; a waiting call inherits prior rejection.
          Update failure can leave earlier rows/internal values changed while public cache is
          stale. Copy failure rolls back via the transaction; tests use a fake rollback, while
          the theme-upload HTTP case proves successful copy and subsequent activation only. Root
          units pin that a failed boot activation is logged without frontend activation or
          cache changes.
        sources:
          - core/server/services/custom-theme-settings/custom-theme-settings-service.js
          - core/server/services/custom-theme-settings/custom-theme-settings-bread-service.js
          - core/server/services/themes/activation-bridge.js
          - core/server/services/themes/activate.js
          - core/server/services/themes/storage.js
          - core/server/api/endpoints/custom-theme-settings.js
        tests:
          - test/unit/server/services/custom-theme-settings/service.test.js
          - test/e2e-api/admin/custom-theme-settings.test.js
          - test/e2e-api/admin/themes.test.js
          - test/unit/server/services/custom-theme-settings/index.test.ts
    unverified: []
    blockers:
      - >-
        Root units pin initial composition without queries or cache changes, boot ordering
        across repeated boots, failed boot activation and one activation pending across repeated
        init. Pending updates or copies across repeated init remain uncharacterized; adoption
        retains their implementation instead of replacing the historical api.
      - >-
        Characterize differing concurrent activations, multiple waiters, rejected activation,
        updates during activation and detached import-triggered activation. The current guard
        is not a queue, and updates reread activeThemeName per row; existing concurrency coverage
        only checks two identical activations. One retained implementation does not resolve
        these operation-level concurrency and pending-work ownership gaps.
      - >-
        Fix or explicitly separate initial visibility behavior: activation computes the public
        cache from DB rows lacking visibility, while update uses merged definitions. The only
        hidden-setting assertion follows an update and cannot catch the activation discrepancy.
        Cover render/preview cache behavior and retained nested definition references.
      - >-
        Test partial activation/update writes and cache divergence with real persistence, plus
        copy rollback/locking under concurrent calls. The fake model verifies transaction
        arguments and simulates rollback; it does not establish database isolation or failure
        atomicity. Existing HTTP copy coverage proves only the successful upload/activate path.
  donations:
    kind: directory
    disposition: migrate
    status: audited
    auditedAt: ad249f4fd584ca5921ef4a72fe44ae986d230264
    migrated: true
    migration:
      complexity: straightforward
      modelImpact: fits
      reason: >-
        Already constructs and retains one repository without listeners or background work.
        Replace the wrapper with the initializer, await it during boot and update Stripe's
        late-bound repository getter so earlier Stripe construction does not read an unready
        service. Existing root tests cover identity and the real Stripe consumer;
        persistence and staff notification ordering should remain unchanged.
      reviewedAt: f276513ba32b7114fd5cc4f7b02d3f82c0c31e3e
      sources:
        - core/server/services/donations/index.ts
        - core/server/services/donations/donation-bookshelf-repository.ts
        - core/server/services/stripe/stripe-service.js
        - core/boot.js
        - test/unit/server/services/donations/index.test.ts
    entryPoints:
      - donations/index.ts
    notes: >-
      Boot creates one ready donation repository; Stripe retains the initializer and reads
      its service only when a donation arrives. Persistence still precedes staff
      notification without a shared transaction or webhook deduplication key.
    contracts:
      exports:
        summary: >-
          The TypeScript root default-exports the shared initializer with async init and a
          guarded service getter exposing DonationBookshelfRepository directly. The
          obsolete CommonJS wrapper is removed; deep modules still export the repository
          and DonationPaymentEvent classes. Repository save requires its receiver and
          resolves without returning the model. The mutable event uses a supplied
          timestamp or the current time. Root tests cover pre-init access, the CommonJS
          default boundary, ready identity and the real Stripe consumer.
        sources:
          - core/server/services/donations/index.ts
          - core/server/services/donations/donation-bookshelf-repository.ts
          - core/server/services/donations/donation-payment-event.ts
          - core/kernel/define-service.ts
        tests:
          - test/unit/server/services/donations/index.test.ts
      construction:
        summary: >-
          The factory resolves the shared DonationPaymentEvent model during init and
          constructs the repository with that reference. Construction only stores the
          model; it performs no writes and acquires no resources. Concurrent
          initialization shares one attempt, and successful initialization retains one
          repository. Boot is the only runtime init caller found. Tests construct Stripe
          before initialization, then use the real repository with only model persistence
          and staff notification stubbed.
        sources:
          - core/server/services/donations/index.ts
          - core/server/services/donations/donation-bookshelf-repository.ts
          - core/server/services/stripe/service.js
          - core/boot.js
        tests:
          - test/unit/server/services/donations/index.test.ts
      consumers:
        summary: >-
          Stripe retains the initializer and supplies checkout handling with a deferred
          getter for service. The handler creates the event, awaits save, then awaits
          staff notification; it does not dispatch the donation through DomainEvents.
          Members event browsing reads the model independently, and optional full database
          export includes the table. Root tests exercise real Stripe-to-repository wiring;
          HTTP tests cover anonymous/member fields, attribution/UTM persistence and staff
          mail through mocked transports. Browsing units pin requested relations rather
          than projection of a newly saved event.
        sources:
          - core/server/services/stripe/service.js
          - core/server/services/stripe/stripe-service.js
          - core/server/services/stripe/services/webhook/checkout-session-event-service.js
          - core/server/services/staff/staff-service-emails.js
          - core/server/services/members/api.js
          - core/server/services/members/members-api/repositories/event-repository.js
          - core/server/data/exporter/table-lists.js
        tests:
          - test/unit/server/services/donations/index.test.ts
          - test/e2e-api/members/donation-checkout-session.test.js
          - >-
            test/unit/server/services/stripe/services/webhooks/checkout-session-event-service.test.js
          - >-
            test/unit/server/services/members/members-api/repositories/event-repository.test.js
      startup:
        summary: >-
          Boot constructs/configures Stripe before awaiting donations init in its main
          Promise.all group. Stripe's getter defers repository access until checkout
          handling, after readiness; reading service before initialization throws
          IncorrectUsageError. The same core startup runs when the frontend is disabled.
          Root tests prove early Stripe construction and later repository use, concurrent
          init and repeated init. Boot tests hold readiness pending and propagate
          initialization failure before job registration/start. Completed-boot HTTP tests
          exercise production wiring; backend-only startup has no focused assertion.
        sources:
          - core/boot.js
          - core/server/services/donations/index.ts
          - core/server/services/stripe/service.js
          - core/server/services/stripe/stripe-service.js
          - core/server/services/stripe/services/webhook/checkout-session-event-service.js
          - core/kernel/define-service.ts
        tests:
          - test/unit/server/services/donations/index.test.ts
          - test/e2e-api/members/donation-checkout-session.test.js
          - test/unit/boot/init-services.test.ts
      lifetime:
        summary: >-
          The initializer retains the repository after successful init, with no reset or
          public replacement setter, and the repository retains its model reference. Tests
          pin concurrent/repeated init identity. Saves still issue independent model adds
          with no serialization, transaction argument, idempotency key or compensation.
          Donation rows have no checkout/event identifier or unique donation constraint,
          and the webhook controller does not deduplicate incoming event IDs. Existing
          tests do not establish duplicate or concurrent webhook-delivery behavior.
        sources:
          - core/server/services/donations/index.ts
          - core/server/services/donations/donation-bookshelf-repository.ts
          - core/kernel/define-service.ts
          - core/server/services/stripe/webhook-controller.js
          - core/server/data/schema/schema.js
        tests:
          - test/unit/server/services/donations/index.test.ts
          - test/e2e-api/members/donation-checkout-session.test.js
      configuration:
        summary: >-
          The root and repository read no configuration or settings. Save maps donation,
          member, attribution, referrer and five UTM fields to model columns;
          event.timestamp is not passed to the model. Checkout policy belongs to
          Members/Stripe, while formatting and notification preferences belong to Staff.
          Root tests pin the complete model-add payload for one anonymous donation; HTTP
          tests check persisted attribution/UTM values and reserved gift metadata
          separation. Schema-invalid inputs and explicit timestamp/created_at behavior
          remain untested at the repository boundary.
        sources:
          - core/server/services/donations/donation-bookshelf-repository.ts
          - core/server/services/donations/donation-payment-event.ts
          - core/server/models/donation-payment-event.js
          - core/server/data/schema/schema.js
          - core/server/services/members/members-api/services/payments-service.js
          - core/server/services/stripe/services/webhook/checkout-session-event-service.js
          - core/server/services/staff/staff-service-emails.js
        tests:
          - test/unit/server/services/donations/index.test.ts
          - test/e2e-api/members/donation-checkout-session.test.js
          - >-
            test/unit/server/services/stripe/services/webhooks/checkout-session-event-service.test.js
      resources:
        summary: >-
          Owns only the repository and its model reference: no timer, listener, transport
          or connection pool is created by the factory. Shared models own database work,
          Stripe owns webhook handling and Staff owns mail delivery. There is no root
          shutdown/drain or pending-operation registry. Root tests hold a model write
          pending and prove checkout handling and staff notification wait for completion;
          they do not establish shutdown or transport-drain behavior.
        sources:
          - core/server/services/donations/index.ts
          - core/server/services/donations/donation-bookshelf-repository.ts
          - core/server/services/stripe/services/webhook/checkout-session-event-service.js
          - core/server/services/staff/staff-service-emails.js
        tests:
          - test/unit/server/services/donations/index.test.ts
          - test/e2e-api/members/donation-checkout-session.test.js
      failures:
        summary: >-
          The shared initializer leaves service unavailable after factory failure and
          allows an explicit retry; Donations has no startup resources to release. Root
          tests prove model-write rejection propagates unchanged and prevents staff
          notification. Notification failure occurs after insertion and cannot undo it.
          Staff sendToStaff awaits sendMail before collecting results, so send rejection
          can escape before allSettled. The webhook controller logs handler errors and
          returns their status or 500, allowing another delivery without local
          deduplication. No focused test covers failure after insertion or database-backed
          rollback behavior.
        sources:
          - core/server/services/donations/index.ts
          - core/kernel/define-service.ts
          - core/server/services/donations/donation-bookshelf-repository.ts
          - core/server/services/stripe/services/webhook/checkout-session-event-service.js
          - core/server/services/stripe/webhook-controller.js
          - core/server/services/staff/staff-service-emails.js
        tests:
          - test/unit/server/services/donations/index.test.ts
          - test/unit/kernel/define-service.test.ts
          - test/e2e-api/members/donation-checkout-session.test.js
          - >-
            test/unit/server/services/stripe/services/webhooks/checkout-session-event-service.test.js
    unverified: []
    blockers:
      - >-
        Cover explicit timestamp/created_at behavior and schema-invalid input through the
        real persistence boundary. Root tests stub model.add, while HTTP tests cover
        selected valid donations.
      - >-
        Characterize duplicate/concurrent webhook delivery and failure after persistence
        but before notification completes. Decide whether idempotency or atomicity should
        change separately; the current path has no checkout/event key or shared
        transaction.
      - >-
        Cover saved-event browsing/export and staff failure channels through real
        consumers, including pending shutdown. Existing single-delivery HTTP tests prove
        selected rows and mail values, not exactly-once delivery or drain.
  email-address:
    kind: directory
    disposition: split
    status: audited
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: straightforward
      modelImpact: fits
      reason: >-
        Already retains one policy instance and reads configuration through live callbacks,
        with no resources to start. Await its initializer before constructing dependants and
        remove or adapt defensive synchronous init calls in welcome-email construction and
        the automated-email endpoint. Keep parser helpers independent; existing wrapper
        tests protect identity and live settings.
      reviewedAt: f276513ba32b7114fd5cc4f7b02d3f82c0c31e3e
      sources:
        - core/server/services/email-address/email-address-service-wrapper.js
        - core/server/services/member-welcome-emails/index.ts
        - core/server/api/endpoints/automated-email-design.js
        - core/boot.js
        - test/unit/server/services/email-address/wrapper.test.ts
      dependencies:
        complexity: straightforward
        reason: >-
          Configuration and settings policy already arrives through live callbacks. The remaining
          dependencies are logging and the Nodemailer address parser. The parser is also
          deep-imported by mail, settings helpers and several email services, so it must remain
          usable independently of the boot-owned policy instance.
        preparation:
          - >-
            Inject logging and parsing from index.ts while preserving the existing live
            configuration callbacks.
          - >-
            Move the shared parser to an appropriate server library in a small preparatory change,
            or introduce an explicit parser factory without forcing unrelated parser callers to
            initialize Email Address.
          - >-
            Await the policy before Email renderer/sender construction captures its service;
            retain GhostMailer's send-time access. Replace defensive synchronous init calls in
            Welcome Emails and the automated-email endpoint.
        reviewedAt: 44ad405bff4a8700af2ed51f833ebc08d25dddff
        sources:
          - core/server/services/email-address/email-address-service-wrapper.js
          - core/server/services/email-address/email-address-service.ts
          - core/server/services/email-service/email-service-wrapper.js
          - core/server/lib/mail/ghost-mailer.js
          - core/server/services/settings-helpers/settings-helpers.js
          - core/server/services/member-welcome-emails/index.ts
          - core/server/api/endpoints/automated-email-design.js
    entryPoints:
      - email-address/index.js
    notes: >-
      Planned split and relocation out of services: the configured Ghost sender-policy
      service belongs in shared; construction and live settings wiring belong in boot.
      Mailbox parsing and formatting belong in an ordinary library, not kernel, while
      delivery compatibility rules belong with sending. Mailbox mechanics now live in
      @tryghost/parse-email-address/mailbox; core/server/lib/mail/format-address-for-delivery.ts
      preserves delivery formatting for existing string-valued callers. The removed private
      [parser](https://github.com/TryGhost/Ghost/blob/bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e/ghost/core/core/server/services/email-address/email-address-parser.ts)
      and its
      [tests](https://github.com/TryGhost/Ghost/blob/bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e/ghost/core/test/unit/server/services/email-address/email-address-parser.test.ts)
      remain historical evidence at auditedAt. Package mailbox tests
      protect the new library boundary; the Core delivery formatter tests protect compatibility.
      The configured service has not moved or adopted defineService. Reassess initialization
      after separating ownership rather than migrating this root in place.
      The policy remains a boot-owned service with dependency and readiness contracts.
      Preserve stable identity, live callbacks and consumers that retain the implementation,
      wrapper or derived sender values. Keep library helpers independent of service startup.
    contracts:
      exports:
        summary: >-
          CommonJS exports an eager wrapper with synchronous init returning undefined and an
          initially undefined service property. The wrapper constructor and named TypeScript
          EmailAddressService class remain deep imports; the class exposes address getters,
          getAddress, getAddressFromString, getMembersSupportAddress and validate. Mailbox
          parsing/formatting now uses the library; delivery formatting belongs to mail.
          There is no reset/shutdown.
        sources:
          - core/server/lib/mail/format-address-for-delivery.ts
          - core/server/services/email-address/index.js
          - core/server/services/email-address/email-address-service-wrapper.js
          - core/server/services/email-address/email-address-service.ts
        tests: []
      construction:
        summary: >-
          First init constructs one EmailAddressService with callbacks over shared config,
          settings helpers and validator, assigning service only after construction succeeds.
          The wrapper is the only production class constructor found. Direct construction of
          wrappers/implementations elsewhere is confined to tests. Parser calls need no init;
          constructing the policy service does not create a mailer or read address values.
        sources:
          - core/server/services/email-address/index.js
          - core/server/services/email-address/email-address-service-wrapper.js
          - core/server/services/email-address/email-address-service.ts
          - core/server/services/settings-helpers/index.js
        tests: []
      consumers:
        summary: >-
          Boot initializes the root. Members' router, newsletter renderer and sending service
          capture service; GhostMailer, settings/newsletters, their serializer, welcome emails
          and automated-email endpoints resolve it through the wrapper. Parser-only callers
          include staff, gifts and settings helpers. Integration/API tests assert real-root
          sender/reply-to policy and validation with mail boundaries mocked. The automated-design
          rejection test stubs validate and proves argument forwarding, HTTP 422 and unchanged
          persisted fields; renderer/sending units also use policy fakes. Inbox-link sender
          semantics remain less covered.
        sources:
          - core/boot.js
          - core/server/services/members/api.js
          - core/server/services/members/members-api/controllers/router-controller.js
          - core/server/services/email-service/email-service-wrapper.js
          - core/server/services/email-service/email-renderer.js
          - core/server/services/email-service/sending-service.js
          - core/server/lib/mail/ghost-mailer.js
          - core/server/services/settings/settings-service.js
          - core/server/services/newsletters/index.ts
          - core/server/api/endpoints/utils/serializers/output/mappers/newsletters.js
          - core/server/services/member-welcome-emails/index.ts
          - core/server/api/endpoints/automated-emails.js
          - core/server/api/endpoints/automated-email-design.js
          - core/server/services/staff/staff-service-emails.js
          - core/server/services/gifts/index.ts
          - core/server/services/settings-helpers/settings-helpers.js
        tests:
          - test/integration/services/q-email-addresses.test.js
          - test/integration/services/member-welcome-emails.test.js
          - test/e2e-api/admin/newsletters.test.js
          - test/e2e-api/admin/settings.test.js
          - test/e2e-api/admin/automated-emails.test.ts
          - test/e2e-api/admin/automated-email-design.test.js
          - test/unit/server/services/email-service/email-renderer.test.js
          - test/unit/server/services/email-service/sending-service.test.js
      startup:
        summary: >-
          Foreground boot initializes email-address after core settings and before Stripe,
          members and email-service construction. Automated-email request handlers also call init
          defensively; Member welcome emails asserts the policy is ready instead. Most consumers have no use-before-init
          guard; captured undefined dependencies would remain undefined. Initialization is
          synchronous composition without asynchronous readiness or validation of configuration.
        sources:
          - core/boot.js
          - core/server/services/email-address/email-address-service-wrapper.js
          - core/server/services/member-welcome-emails/index.ts
          - core/server/api/endpoints/automated-emails.js
          - core/server/api/endpoints/automated-email-design.js
          - core/server/services/members/api.js
          - core/server/services/email-service/email-service-wrapper.js
        tests: []
      lifetime:
        summary: >-
          Repeated init retains the same service and callback references; wrapper tests assert
          that identity. Server stop does not reset it. Captured implementation consumers would
          not follow replacement of wrapper.service. Welcome emails additionally cache derived
          newsletter sender strings, refreshed on loading welcome emails and test sends.
          getAddress can mutate the caller's preferred addresses and return those same objects.
        sources:
          - core/server/services/email-address/email-address-service-wrapper.js
          - core/server/services/email-address/email-address-service.ts
          - core/server/services/email-service/email-service-wrapper.js
          - core/server/services/members/api.js
          - core/server/services/member-welcome-emails/index.ts
          - core/server/services/automations/welcome-email-automation-poll.ts
          - core/server/ghost-server.ts
        tests:
          - test/unit/server/services/email-address/wrapper.test.ts
      configuration:
        summary: >-
          Each use reads managed-email enablement, sending/fallback domains, fallback address,
          default sender and support address through callbacks. Missing managed-email options
          yield false/null;
          fallback strings are parsed on each read. Managed policy applies default/domain and
          optional fallback rules; disabled policy permits valid requested addresses. Wrapper
          tests change values on one retained instance; integration tests use test-only config
          mutation after boot. These prove accessor rereads, not production hot reload of the
          immutable config tree. Mailbox parsing tests live in the package; Core delivery
          tests cover name escaping and character removal.
        sources:
          - core/server/services/email-address/email-address-service-wrapper.js
          - core/server/services/email-address/email-address-service.ts
          - core/server/services/settings-helpers/settings-helpers.js
        tests:
          - test/unit/server/services/email-address/wrapper.test.ts
          - test/unit/server/services/email-address/email-address-service.test.ts
          - test/unit/server/lib/mail/format-address-for-delivery.test.ts
          - test/integration/services/q-email-addresses.test.js
      resources:
        summary: >-
          This root owns only the wrapper, policy object and callback references. It borrows
          config, settings helpers, validator and logging, and owns no transport, database,
          timer, event subscription or pending asynchronous work. Nodemailer addressparser is
          used only for parsing. Mailers, verification tokens and delivery work belong to its
          consumers; initializing email-address does not acquire or dispose those resources.
        sources:
          - core/server/services/email-address/index.js
          - core/server/services/email-address/email-address-service-wrapper.js
          - core/server/services/email-address/email-address-service.ts
          - core/server/services/settings-helpers/index.js
        tests: []
      failures:
        summary: >-
          Initialization failure leaves service unset for a later attempt. Callback/parser or
          malformed-input exceptions propagate synchronously. Invalid reply-to is removed and
          logged; invalid from falls back to the default without revalidation. Missing requested
          fallback logs then uses normal policy. validate returns allowed/verification/reason
          data; HTTP consumers enforce it with ValidationError. Tests cover fallback outcomes,
          reply-to log severity and API rejection, not initialization or callback faults.
        sources:
          - core/server/services/email-address/email-address-service-wrapper.js
          - core/server/services/email-address/email-address-service.ts
          - core/server/api/endpoints/utils/validate-email-sender-fields.ts
          - core/server/services/settings/settings-bread-service.js
          - core/server/services/newsletters/newsletters-service.js
        tests:
          - test/unit/server/services/email-address/email-address-service.test.ts
          - test/unit/server/lib/mail/format-address-for-delivery.test.ts
          - test/e2e-api/admin/automated-emails.test.ts
          - test/e2e-api/admin/newsletters.test.js
    unverified: []
    blockers:
      - >-
        Add exact root facade, startup ordering, use-before-init and failed-construction retry
        contracts. Existing wrapper tests instantiate the wrapper class directly; real consumer
        tests do not deliberately exercise missing readiness or service replacement on restart.
      - >-
        Cover captured implementation consumers and welcome-email cached sender values across
        configuration changes, reload and replacement. Existing welcome tests often recreate
        their service; cached design-content coverage does not establish cached sender timing.
        Add a real members inbox-link assertion for the transformed sender.
      - >-
        Preserve getAddress input mutation/aliasing, case-sensitive default/domain matching,
        invalid-default and malformed-fallback behavior. Parser acceptance is not address
        validation; current policy tests cover useful examples but not these complete contracts.
      - >-
        Add callback/parser/validator failure contracts and verify propagation through mail/API
        callers. Missing-fallback behavior is asserted without its error log; live getter tests
        and mocked provider requests do not prove every failure or fallback-domain combination.
  email-analytics:
    kind: directory
    disposition: migrate
    status: audited
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: complex
      modelImpact: fits
      reason: >-
        Compose the newsletter, automation and gift executors as one ready aggregate instead
        of publishing them sequentially behind only the newsletter guard. Failure after
        metric registration or one executor must not leave an unretryable partial root.
        Preserve explicit dependencies and the separate job scheduler; three executors and
        independent cursors fit one returned composition without adding lifecycle hooks to
        the helper.
      reviewedAt: f276513ba32b7114fd5cc4f7b02d3f82c0c31e3e
      sources:
        - core/server/services/email-analytics/index.ts
        - core/server/services/email-analytics/email-analytics-service-wrapper.ts
        - core/server/services/email-analytics/jobs/index.js
        - core/boot.js
        - test/unit/server/services/email-analytics/index.test.ts
      dependencies:
        complexity: complex
        reason: >-
          The main root explicitly wires three executors, while jobs/index.js independently creates
          a scheduler used by Email, Gifts and Automations. Newsletter processing also constructs
          Email implementation classes, and provider creation and logging remain inside analytics
          helpers. A root-only move must preserve these separate retained instances, late job lookup
          and call-time provider configuration.
        preparation:
          - >-
            Bring the scheduler composition under the root while preserving its late jobs getter,
            existing scheduling gates and public scheduling callers.
          - >-
            Inject the Mailgun client factory, logger and query/helper operations through the
            wrappers and processors; keep client construction per fetch and config/settings reads
            live.
          - >-
            Define the ready aggregate and dependency order for member repositories, gift delivery
            and automation APIs before replacing the current three getters.
        reviewedAt: 44ad405bff4a8700af2ed51f833ebc08d25dddff
        sources:
          - core/server/services/email-analytics/index.ts
          - core/server/services/email-analytics/jobs/index.js
          - core/server/services/email-analytics/email-analytics-service-wrapper.ts
          - core/server/services/email-analytics/fetch-mailgun-events.ts
          - core/server/services/email-service/email-service-wrapper.js
          - core/server/services/gifts/index.ts
          - core/boot.js
    entryPoints:
      - email-analytics/index.ts
      - email-analytics/jobs/index.js
    notes: >-
      Preserve independent newsletter, automation and gift pipelines and their separate
      scheduling entry point. Distinguish job completion, cursor persistence and downstream
      event settlement; partial initialization and same-process restart remain obligations.
    contracts:
      exports:
        summary: >-
          Named TypeScript exports are synchronous init and guarded getNewsletters,
          getAutomations and getGifts. Getters return retained wrappers; init returns undefined.
          Wrappers expose service plus startFetch and individual count-returning fetch methods;
          service exposes mutable status references, scheduling/cancellation and detailed fetch
          results. The CommonJS jobs entry exports three receiver-independent async scheduling
          functions, all no-ops in test environments. Root units substitute wrapper constructors
          and assert pre-init guards, distinct identities, constructor arguments and repeated
          init retention. Admin HTTP snapshots assert initial newsletter status, not scheduling
          or cancellation completion. Exact return/receiver behavior is otherwise source-only.
        sources:
          - core/server/services/email-analytics/index.ts
          - core/server/services/email-analytics/email-analytics-service-wrapper.ts
          - core/server/services/email-analytics/email-analytics-service.ts
          - core/server/services/email-analytics/jobs/index.js
          - core/server/api/endpoints/emails.js
        tests:
          - test/unit/server/services/email-analytics/index.test.ts
          - test/e2e-api/admin/emails.test.js
          - test/e2e-api/admin/__snapshots__/emails.test.js.snap
      construction:
        summary: >-
          Init constructs one Queries retaining the supplied knex, newsletter storage/event
          processor, and three wrappers with separate job names, tags and cursor seeds. Each
          wrapper constructs an EmailAnalyticsService; each fetch creates a batch processor,
          while newsletter storage, pending updates and provider-ID cache are shared across its
          fetches. MailgunClient and its SDK client are created per provider fetch. The separate
          jobs entry eagerly constructs a scheduler borrowing models/config; its callback
          resolves the current jobs instance per registration. Boot is the production root
          initializer; no other deep implementation composition was found. Root units check
          injected constructor arguments/counter registration, not real constructor failures.
        sources:
          - core/server/services/email-analytics/index.ts
          - core/server/services/email-analytics/email-analytics-service-wrapper.ts
          - core/server/services/email-analytics/email-analytics-service.ts
          - core/server/services/email-analytics/jobs/index.js
          - core/server/services/email-analytics/jobs/email-analytics-job-scheduler.ts
          - core/server/services/email-analytics/fetch-mailgun-events.ts
          - core/server/services/email-analytics/lib/queries.ts
          - core/server/services/email-service/email-event-processor.js
          - core/server/services/email-service/newsletter-email-event-storage.js
          - core/server/lib/mailgun/mailgun-client.js
        tests:
          - test/unit/server/services/email-analytics/index.test.ts
          - test/unit/server/services/email-analytics/fetch-mailgun-events.test.ts
      consumers:
        summary: >-
          Boot passes the three wrappers to jobs handlers that retain them and return startFetch
          in separate two-slot queues. Background boot schedules all three; newsletter sends,
          automation sends and accepted gift deliveries request their own schedule with the
          recent-send lookup bypass. The Admin emails endpoint reads current newsletter service
          status and requests backfill/cancel via root getters. Booted-backend integration tests
          dispatch each job with Mailgun fetching stubbed, assert recipient/outcome and selected
          aggregate writes, its cursor progress and unchanged sibling cursor rows. Automation
          integration also asserts replay does not double-count opens. These are database/job
          composition tests, not live transport, real cron or HTTP backfill/cancel coverage.
        sources:
          - core/boot.js
          - core/server/services/jobs-service/register-job-handlers.ts
          - core/server/services/email-service/email-service-wrapper.js
          - core/server/services/email-service/email-service.js
          - core/server/services/automations/service.ts
          - core/server/services/automations/poll.ts
          - core/server/services/gifts/index.ts
          - core/server/services/gifts/gift-delivery-service.ts
          - core/server/api/endpoints/emails.js
          - core/server/web/api/endpoints/admin/routes.js
        tests:
          - test/integration/services/email-analytics/jobs.test.ts
          - test/integration/services/email-analytics/automation-email-analytics.test.js
          - test/unit/server/services/email-analytics/jobs.test.ts
          - test/e2e-api/admin/emails.test.js
      startup:
        summary: >-
          Boot initializes analytics synchronously after constructing gifts and invoking
          members.init earlier in the service group, then registers handlers and starts jobs. Analytics init
          itself is not feature-gated. Background scheduling starts after server readiness,
          skips test environments and uses allSettled so individual scheduling failures are
          logged without suppressing later background services. Scheduler registration requires
          both enable flags and recent eligible sends, unless its caller bypasses that lookup.
          Boot VM tests use mocked collaborators: they assert initializer invocation ordering
          and continuation after two scheduling failures; separate held-promise cases prove
          waits for Stripe, async peer initializers and jobs startup. Separate VM cases cover
          selected environment/recovery barriers without asserting analytics schedule calls;
          scheduler units establish those gates. Real-boot HTTP remains ready while controlled
          newsletter recovery is held, with analytics scheduling disabled in that test. These
          do not exercise partial analytics construction or real background provider readiness.
        sources:
          - core/boot.js
          - core/server/services/email-analytics/index.ts
          - core/server/services/email-analytics/jobs/index.js
          - core/server/services/email-analytics/jobs/email-analytics-job-scheduler.ts
          - core/server/services/jobs-service/register-job-handlers.ts
        tests:
          - test/unit/boot/init-services.test.ts
          - test/unit/boot/init-background-services.test.ts
          - test/e2e-server/boot-background.isolated.test.ts
          - test/unit/server/services/email-analytics/email-analytics-job-scheduler.test.ts
          - test/integration/services/email-analytics/jobs.test.ts
      lifetime:
        summary: >-
          Publication is sequential and only newsletters guards init: a later constructor
          failure leaves partial state that retry cannot complete. No reset/disposer exists.
          Wrappers retain restored/fetching flags and in-memory cursors; jobs-table cursors and
          backfill metadata survive process replacement. Scheduler flags survive successful
          registration, with rejection clearing the flag, but have no shutdown reset. Wrapper
          units hold a fetch to assert overlapping startFetch skips while a sibling progresses,
          and hold a continuation to prove the returned promise remains pending. Direct fetch
          methods bypass that guard. First restoration is marked attempted before awaiting it;
          overlapping initial calls and retry after restoration failure are not characterized.
          Constructor-fake root tests prove normal identity retention, not same-module reboot.
        sources:
          - core/server/services/email-analytics/index.ts
          - core/server/services/email-analytics/email-analytics-service-wrapper.ts
          - core/server/services/email-analytics/email-analytics-service.ts
          - core/server/services/email-analytics/jobs/email-analytics-job-scheduler.ts
          - core/server/services/email-analytics/lib/queries.ts
          - core/server/adapters/jobs/InMemoryJobsBackend.ts
        tests:
          - test/unit/server/services/email-analytics/index.test.ts
          - test/unit/server/services/email-analytics/email-analytics-service-wrapper.test.ts
          - test/unit/server/services/email-analytics/email-analytics-job-scheduler.test.ts
          - test/unit/server/services/email-analytics/email-analytics-service.test.ts
      configuration:
        summary: >-
          Defaults enable analytics/background scheduling, disable batch processing, and disable
          open-throughput metrics with threshold 500. Root captures optional site Mailgun tag,
          pipeline names/seeds and collaborators; wrappers/processors retain config/settings
          facades and read processing/metric options during work. Each provider fetch rereads
          Mailgun config or settings, with configured bulkEmail.mailgun taking precedence, and
          optional fallback domain. Mailgun requests use AND tags, OR event types, ascending
          pages of 300 and a configured SDK timeout of 60 seconds. Gifts omit the opens pass.
          Scheduler units assert flag gates, bypasses and seeded five-minute cron values; fetch
          units assert request options with a stubbed client. Nock Mailgun units assert selected
          paging/domain-safe cursors, not timeout completion. Test overrides do not establish
          production hot reload of captured tags, collaborators or registered schedules.
        sources:
          - core/shared/config/defaults.json
          - core/shared/config/SCHEMA.md
          - core/server/services/email-analytics/index.ts
          - core/server/services/email-analytics/email-analytics-service-wrapper.ts
          - core/server/services/email-analytics/jobs/email-analytics-job-scheduler.ts
          - core/server/services/email-analytics/fetch-mailgun-events.ts
          - core/server/services/email-analytics/newsletter-email-analytics-batch-processor.js
          - core/server/lib/mailgun/mailgun-client.js
        tests:
          - test/unit/server/services/email-analytics/index.test.ts
          - test/unit/server/services/email-analytics/email-analytics-job-scheduler.test.ts
          - test/unit/server/services/email-analytics/email-analytics-service-wrapper.test.ts
          - test/unit/server/services/email-analytics/fetch-mailgun-events.test.ts
          - test/unit/server/lib/mailgun/mailgun-client.test.js
      resources:
        summary: >-
          The root borrows DB/models/services/metrics and owns retained cursors, newsletter
          pending-update maps and a provider-ID cache without eviction. Recurring timers and
          queues belong to jobs; root has no stop/abort/drain. Default backend clears timers
          and pending work and waits only to its shutdown deadline, without canceling running
          provider/DB work. Awaited startFetch includes recursive continuation, but not detached
          metadata writes or dispatched DomainEvents subscribers; newsletter failures/complaints
          include a 70ms yield, not an event drain. Job units prove a handler awaits an injected
          executor and fake-time recurring dispatch reaches the real backend once; wrapper
          continuation is tested separately. Neither proves real analytics provider work and
          downstream events settle before GhostServer shutdown or restart.
        sources:
          - core/boot.js
          - core/server/services/email-analytics/email-analytics-service-wrapper.ts
          - core/server/services/email-analytics/email-analytics-service.ts
          - core/server/services/email-analytics/jobs/email-analytics-job-scheduler.ts
          - core/server/services/jobs-service/register-job-handlers.ts
          - core/server/services/jobs-service/jobs-service.ts
          - core/server/adapters/jobs/InMemoryJobsBackend.ts
          - core/server/services/email-service/email-event-processor.js
          - core/server/services/email-service/newsletter-email-event-storage.js
          - core/server/lib/mailgun/mailgun-client.js
        tests:
          - test/unit/server/services/email-analytics/jobs.test.ts
          - test/unit/server/services/email-analytics/email-analytics-service-wrapper.test.ts
          - test/integration/services/email-analytics/jobs.test.ts
      failures:
        summary: >-
          Init throws synchronously; scheduler registration rejection propagates and permits
          retry. Wrapper startFetch logs and swallows ordinary fetch errors, permitting later ticks;
          its injected restore rejection test propagates, but real restoreScheduled catches
          read errors internally and the wrapper does not retry restoration. Direct service
          fetches reject provider/final-aggregation errors, rewind the in-memory cursor on
          fetch failure and retain prior published progress; intermediate aggregation errors
          log and continue. Service units assert these paths with fake queries/processors.
          SQLite query tests assert timestamp-write errors are swallowed and status-write
          errors reject; metadata writes also swallow errors in source. Cancel/completion
          metadata writes are not awaited. No transaction spans provider fetches, recipient
          writes, event dispatch, aggregation and cursors. Booted integrations establish selected
          stored results and replay behavior, not rollback or exactly-once side effects.
        sources:
          - core/server/services/email-analytics/index.ts
          - core/server/services/email-analytics/email-analytics-service-wrapper.ts
          - core/server/services/email-analytics/email-analytics-service.ts
          - core/server/services/email-analytics/lib/queries.ts
          - core/server/services/email-analytics/jobs/email-analytics-job-scheduler.ts
          - core/server/services/email-analytics/newsletter-email-analytics-batch-processor.js
          - core/server/services/email-analytics/automation-email-analytics-batch-processor.ts
          - core/server/services/email-analytics/gift-email-analytics-batch-processor.ts
          - core/server/services/email-service/email-event-processor.js
          - core/server/services/email-service/newsletter-email-event-storage.js
        tests:
          - test/unit/server/services/email-analytics/email-analytics-service-wrapper.test.ts
          - test/unit/server/services/email-analytics/email-analytics-service.test.ts
          - test/unit/server/services/email-analytics/email-analytics-job-scheduler.test.ts
          - test/unit/server/services/email-analytics/queries.test.ts
          - test/integration/services/email-analytics/jobs.test.ts
          - test/integration/services/email-analytics/automation-email-analytics.test.js
          - test/integration/services/email-service/newsletter-email-event-storage.test.js
    unverified: []
    blockers:
      - >-
        Characterize partial root initialization and retry, including metric registration
        before publication and a later wrapper constructor failing after newsletters is set.
        Current identity tests replace all wrapper constructors with successful fakes.
      - >-
        Define shutdown/restart for retained wrappers and the separately composed scheduler.
        Successful schedule flags outlive backend timer clearing; registration after backend
        stop may resolve without a timer. Guards do not re-read config for existing schedules.
      - >-
        Test overlapping first startFetch calls while restoration is pending and failure on
        that first restore. Restoration is marked attempted before the await and is not retried;
        ordinary direct fetch methods bypass the wrapper guard and share newsletter buffers.
      - >-
        Cover HTTP backfill/cancel completion, detached metadata writes and DB failure status.
        Timestamp/metadata persistence can fail while calls resolve; status-write or processor
        construction failure can leave a fetch status running. Existing cancel assertions check
        calls/state, not durable cancellation before return or shutdown.
      - >-
        Verify actual analytics continuation and provider/DB work across job shutdown deadlines,
        plus downstream DomainEvents and pending newsletter updates. Injected waiting tests and
        successful booted job persistence do not prove a full drain or atomic replay behavior.
      - >-
        Preserve provider paging/cursor behavior and captured-versus-current configuration
        boundaries. Changing credentials/domain while retaining cursors or site tags has no
        migration coverage; batch failures can retain pending writes and emit events before
        final storage/aggregation without rollback of earlier effects.
  email-service:
    kind: directory
    disposition: review
    status: audited
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: complex
      modelImpact: may-change
      reason: >-
        Repeated init deliberately re-arms a stopped batch sender and registers
        pre-stop/drain callbacks with the new GhostServer while retaining the delivery
        graph. A once-only factory cannot silently replace that per-boot work. Decide how
        construction, server attachment and restart are separated; also make partial
        listener/controller construction retry-safe and await sibling dependencies before
        capture. This is a concrete test of the model's lifecycle boundary.
      reviewedAt: f276513ba32b7114fd5cc4f7b02d3f82c0c31e3e
      sources:
        - core/server/services/email-service/email-service-wrapper.js
        - core/server/services/email-service/batch-sending-service.js
        - core/server/services/email-service/README.md
        - core/boot.js
        - test/unit/server/services/email-service/batch-sending-service.test.js
      dependencies:
        complexity: complex
        reason: >-
          The wrapper captures ready member, attribution, feedback and address instances but passes
          other live wrappers and an independently composed analytics scheduler. Rendering
          additionally imports member gating, address parsing, filesystem/template and image
          helpers, including deferred heavy libraries. Email analytics constructs some of the same
          implementation classes separately, so one constructor edit cannot establish the whole
          boundary.
        preparation:
          - >-
            Make the renderer, sending, provider and event-processing collaborators accept explicit
            dependencies from their respective roots; preserve lazy heavy renderer loading and
            configuration reads.
          - >-
            Establish readiness before capturing member, attribution, feedback and address
            instances, retaining explicit deferred access where a genuine dependency cycle exists.
          - >-
            Coordinate the analytics scheduler and shared event-processing construction with Email
            Analytics, keeping retained delivery graph ownership separate from per-server shutdown
            attachment.
        reviewedAt: 44ad405bff4a8700af2ed51f833ebc08d25dddff
        sources:
          - core/server/services/email-service/email-service-wrapper.js
          - core/server/services/email-service/email-renderer.js
          - core/server/services/email-service/mailgun-email-provider.js
          - core/server/services/email-service/email-event-processor.js
          - core/server/services/email-analytics/index.ts
          - core/server/services/email-analytics/jobs/index.js
          - core/boot.js
    entryPoints:
      - email-service/index.js
      - email-service/newsletter-email-event-storage.js
      - email-service/email-event-processor.js
    notes: >-
      Decide who registers pre-stop and drain for in-flight sends before migrating: each
      init attaches them to the server it receives, and defineService has no shutdown hook.
      Preserve the application-owned delivery wrapper, retained collaborators, newsletter
      queue and separate pre-stop/drain phases. Analytics separately constructs the two
      deep event-processing exports. Recorded facets include source-only lifecycle risks;
      the existing business tests do not establish migration readiness.
    contracts:
      exports:
        summary: >-
          CommonJS exports one EmailServiceWrapper instance. init is synchronous and publishes
          mutable renderer, service, controller and batchSendingService fields;
          registerShutdownTasks registers callbacks that resolve the retained batch sender when
          invoked. getPostUrl maps a post/page through the API URL helper. Implementation methods
          use their receiver. Analytics imports
          NewsletterEmailEventStorage and EmailEventProcessor constructors directly; other
          runtime support imports include SendEmailJob, event classes, sending-status serializers
          and welcome-email template helpers/styles. PostEmailHandler imports the implementation's
          two retry error-code constants. Wrapper units assert only the pages/posts
          argument passed to the URL helper, not root identity, initialized shape or URL output.
        sources:
          - core/server/services/posts/post-email-handler.js
          - core/server/services/email-service/index.js
          - core/server/services/email-service/email-service-wrapper.js
          - core/server/services/email-analytics/index.ts
          - core/server/services/jobs-service/register-job-handlers.ts
          - core/server/services/member-welcome-emails/member-welcome-email-renderer.js
          - core/server/api/endpoints/utils/serializers/output/emails.js
        tests:
          - test/unit/server/services/email-service/email-service-wrapper.test.js
      construction:
        summary: >-
          Root evaluation creates the wrapper; first init requires jobsService and builds
          Mailgun client/provider, translation, renderer, sender, segmenter, warming, batch
          sender, status service, EmailService and controller; EmailService retains that status
          service for retry eligibility reads. It captures members.api.members,
          members.verificationTrigger, email-address.service, attribution and feedback instances;
          link tracking is retained as a wrapper and resolved through its service field later.
          It subscribes to locale edits, retains batchSendingService, optionally registers
          GhostServer pre-stop/cleanup callbacks, then publishes renderer, service and controller
          in that order. A truthy service keeps the original graph, re-arms the retained batch
          sender and registers callbacks with the supplied GhostServer; it does not replace jobs.
          These exact construction/guard/publication
          contracts have no dedicated wrapper assertions. Analytics separately constructs its
          storage/processor with borrowed DB, members, suppression and metrics dependencies.
        sources:
          - core/server/services/email-service/email-service-wrapper.js
          - core/server/services/email-service/email-renderer.js
          - core/server/services/email-service/email-service.js
          - core/server/services/email-service/batch-sending-service.js
          - core/server/services/email-analytics/index.ts
        tests: []
      consumers:
        summary: >-
          Boot initializes the root, retains service in the send-email handler, and later calls
          recovery through the wrapper. PostsService and PostEmailHandler retain service for
          preflight/create/schedule/retry; email-preview and email endpoints resolve controller on each
          call. Output mapping resolves renderer/service and skips replacement when absent;
          comment email rendering resolves renderer for unsubscribe links. Analytics owns the
          deep processors; suppression and members last-seen consume their domain events;
          welcome rendering reuses helpers/styles. Real HTTP tests assert preview snapshots,
          two test-mail provider payloads with distinct message IDs, old-email placeholder
          removal, status responses and republish/retry results. Batch integration exercises
          HTTP publication through real composition/DB to a mocked provider. Atomic-publication
          integration holds an outer transaction and asserts no send dispatch before commit;
          insert/outer-transaction failures leave no email or published post. Its dispatch is
          stubbed, so these are persistence/scheduling assertions, not provider-delivery evidence.
          Handler units
          assert the injected job, two-slot email queue and rejection propagation. These do not
          prove every consumer survives root replacement or sibling reinitialization.
        sources:
          - core/boot.js
          - core/server/services/posts/posts-service-instance.js
          - core/server/services/posts/post-email-handler.js
          - core/server/api/endpoints/email-previews.js
          - core/server/api/endpoints/emails.js
          - core/server/api/endpoints/utils/serializers/output/mappers/emails.js
          - core/server/services/comments/comments-service-emails.js
          - core/server/services/jobs-service/register-job-handlers.ts
          - core/server/services/email-analytics/index.ts
          - core/server/services/email-suppression-list/mailgun-email-suppression-list.js
          - core/server/services/members-events/last-seen-at-updater.js
          - core/server/services/member-welcome-emails/member-welcome-email-renderer.js
        tests:
          - test/integration/services/email-service/atomic-publish.test.ts
          - test/e2e-api/admin/email-previews.test.js
          - test/e2e-api/admin/emails.test.js
          - test/e2e-api/admin/email-retry.test.ts
          - test/integration/services/email-service/batch-sending.test.js
          - test/unit/server/services/jobs-service/register-job-handlers.test.ts
      startup:
        summary: >-
          Boot initializes email-address and awaits member-attribution and audience-feedback before
          entering the required-service array. It invokes members before email-service in that
          array, then registers handlers and starts jobs after the group settles. Members still
          relies on sibling publication timing; the array does not await each initializer before
          invoking the next. Background recovery runs after
          readiness, is skipped for test-prefixed environments and catches/logs its failure
          before continuing. scheduleEmail awaits dispatch, not completion of a send; emailJob
          refetches and transactionally claims pending/failed email status. VM boot tests hold
          email init and assert dependent jobs remain stopped, propagate its exact failure,
          cover the test-environment skip and hold recovery before later background work.
          Real-boot HTTP tests assert jobs readiness and a usable server while controlled
          recovery remains pending, then continuation after release. They do not exercise real
          recovery work or prove the sibling publication assumptions. Job units assert dispatch
          payload and rejection; integration manually invokes recovery on a booted root
          and asserts pending batches submit, submitted batches skip, and orphan batches fail
          the parent without another provider call.
        sources:
          - core/boot.js
          - core/server/services/email-service/email-service-wrapper.js
          - core/server/services/email-service/batch-sending-service.js
          - core/server/services/email-service/email-service.js
          - core/server/services/jobs-service/register-job-handlers.ts
        tests:
          - test/unit/boot/init-services.test.ts
          - test/unit/boot/init-background-services.test.ts
          - test/e2e-server/boot-readiness.isolated.test.ts
          - test/e2e-server/boot-background.isolated.test.ts
          - test/unit/server/services/email-service/batch-sending-service.test.js
          - test/integration/services/email-service/resume-interrupted-sends.test.js
      lifetime:
        summary: >-
          Wrapper identity, implementation graph and translation listener persist without
          reset/dispose. Pre-stop sets the batch sender's shutdown flag; repeated wrapper init
          clears it through onStart and registers callbacks on the supplied server. Registration
          has no same-server deduplication. These restart behaviors are source-only. Cleanup
          waits with allSettled for a snapshot of tracked sendEmail work. That tracking spans
          preparation/submission but ends before emailJob writes the final parent Email status.
          Batch units hold preparation or sendBatch pending and assert drain stays pending;
          they also assert pre-stop stops further claims and sibling workers settle before an
          error surfaces. Accounting integration resumes frozen recipients using a newly
          constructed batch sender, not the same stopped wrapper. Renderer retains its first
          template-compilation promise, including rejection. Body caches are per sendBatches
          invocation; analytics owns separate provider-ID and pending-event maps. No cited test
          establishes same-wrapper restart, listener removal or final parent-write drain.
        sources:
          - core/server/services/email-service/email-service-wrapper.js
          - core/server/services/email-service/batch-sending-service.js
          - core/server/services/email-service/email-renderer.js
          - core/server/services/email-service/email-event-processor.js
          - core/server/services/email-service/newsletter-email-event-storage.js
        tests:
          - test/unit/server/services/email-service/batch-sending-service.test.js
          - test/integration/services/email-service/recipient-accounting.test.ts
      configuration:
        summary: >-
          Init captures batchCreationConcurrency, sibling instances and the initial locale;
          batch construction selects retry policy from NODE_ENV, with retries disabled by
          default in development/test. Retained config/settings facades are read per operation
          for Mailgun credentials, batch size/window/tag/message IDs, warming, recovery age,
          verification message and rendering settings. Configured Mailgun overrides settings;
          each request creates an SDK client with a 60-second timeout. Locale edits request
          translation changes without awaiting them; renderer helpers retain that translation
          function while reading locale/timezone/settings later. Preflight counts the audience
          and warming allocation before publication. createEmail requires matching newsletter/
          filter values, uses those counts without rechecking limits or recounting, and writes
          through the supplied transaction. Units assert transaction forwarding, no recount or
          dispatch, retained counts including zero, and rejection of missing/mismatched preflight.
          New rows capture tracking/filter/source/count data; prepared membership freezes at
          prepared_at. Tests assert selected injected options, concurrency validation, warming
          enablement/day values, recovery cutoff filters and configured message-ID payloads.
          Test overrides do not establish production hot reload or retained-reference rebinding.
        sources:
          - core/server/services/email-service/email-service-wrapper.js
          - core/server/services/email-service/email-service.js
          - core/server/services/email-service/email-renderer.js
          - core/server/services/email-service/helpers/register-helpers.js
          - core/server/services/email-service/batch-sending-service.js
          - core/server/services/email-service/domain-warming-service.ts
          - core/server/services/email-service/mailgun-email-provider.js
          - core/server/lib/mailgun/mailgun-client.js
        tests:
          - test/unit/server/services/email-service/recipient-preparation.test.ts
          - test/unit/server/services/email-service/domain-warming-service.test.ts
          - test/unit/server/services/email-service/email-service.test.js
          - test/unit/server/services/email-service/mailgun-email-provider.test.js
          - test/e2e-api/admin/email-previews.test.js
      resources:
        summary: >-
          Delivery borrows jobs, DB/models, image cache/storage and sibling services. It owns
          its locale listener, compiled template promise, active-send set and per-send body
          cache. Posts saves the publication and pending email in one transaction, opening one
          or joining the caller's. Scheduling follows its own transaction's completion;
          caller-owned transactions register a detached callback on executionPromise fulfillment
          through afterCommit. There is no durable outbox for this callback. Preparation retains
          the selected audience IDs, bounds page workers, writes
          each page transactionally with a stable batch identity, verifies uncertain commits,
          and drains workers on failure. Retry waits own timers/abort listeners; aborting those
          waits does not cancel an active transaction/provider request. Two workers submit
          batches per email; jobs registers two concurrent email handlers. Rendering can read
          templates/images/posts and persist tracking links through borrowed helpers; image
          lookup has shared cache/in-flight work and its own request timeout. The installed
          Mailgun SDK uses bundled Axios with configured request timeouts and default HTTP agents;
          its timeout path destroys the request, but this wrapper exposes no cancellation or
          agent-disposal handle. This is source evidence, not tested deadline/shutdown behavior.
          Analytics deep
          objects own an unbounded provider-ID map, pending timestamp maps and metric
          registrations; domain-event dispatch does not await subscribers. Units exercise
          bounded preparation workers, drain gates and cached-image fallback using fakes;
          these do not prove provider socket disposal or whole-server shutdown completion.
        sources:
          - core/server/lib/after-commit.ts
          - core/server/services/posts/posts-service.js
          - core/server/services/email-service/email-service-wrapper.js
          - core/server/services/email-service/batch-sending-service.js
          - core/server/services/email-service/recipient-preparation.ts
          - core/server/services/email-service/email-renderer.js
          - core/server/services/email-service/email-event-processor.js
          - core/server/services/email-service/newsletter-email-event-storage.js
          - core/server/services/jobs-service/register-job-handlers.ts
          - core/server/lib/mailgun/mailgun-client.js
          - package.json
          - core/server/lib/image/index.js
          - core/server/lib/image/cached-image-size-from-url.js
          - core/server/lib/image/image-size.js
        tests:
          - test/integration/services/email-service/atomic-publish.test.ts
          - test/unit/server/services/email-service/recipient-preparation.test.ts
          - test/unit/server/services/email-service/batch-sending-service.test.js
          - test/unit/server/services/email-service/email-renderer.test.js
      failures:
        summary: >-
          Wrapper failures after subscription/cleanup registration are not rolled back; service
          publication before controller can prevent retry after a later failure. createEmail
          rejects mismatched preflight with UpdateCollisionError; it inserts without dispatching.
          scheduleEmail converts dispatch failure to a failed row and logs analytics-scheduling
          failure. retryEmail rereads persisted eligibility, rejects a non-failed parent or any
          submitting batch, then claims transactionally and restores failed status on dispatch
          rejection. Status responses expose retryable for failed sends. Eligibility and claim
          are separate reads; these are not a cross-process ownership guarantee.
          Recovery isolates individual emails, fails stale/unsendable ones and requeues eligible
          work without rechecking limits. Batch failures use bounded retries, preserve selected
          non-retryable integrity errors and normally persist failed status; shutdown leaves
          interrupted parents submitting. Provider acceptance before failed DB persistence is
          uncertain, not an exactly-once guarantee. Unit tests assert dispatch compensation,
          preflight mismatch and stale/unknown-outcome retry rejection with a fake status service;
          SQLite status tests read changed batch state and reject invalid stored status. Selected
          retry/drain failures and real-DB orphan handling are asserted with a
          mocked provider. Renderer caches compilation rejection but tolerates image failures.
          Analytics storage flushes maps sequentially and clears only after full success;
          dispatch/storage effects are not atomic. Its integration asserts selected delivered
          timestamps in sequential/batched modes with fetch/cursor stubs, not live-provider
          semantics or a global rollback/drain guarantee.
        sources:
          - core/server/services/email-service/sending-status-serializers.ts
          - core/server/services/email-service/sending-status.ts
          - core/server/services/email-service/sending-status-service.ts
          - core/server/services/email-service/email-service-wrapper.js
          - core/server/services/email-service/email-service.js
          - core/server/services/email-service/batch-sending-service.js
          - core/server/services/email-service/sending-service.js
          - core/server/services/email-service/mailgun-email-provider.js
          - core/server/services/email-service/email-renderer.js
          - core/server/services/email-service/email-event-processor.js
          - core/server/services/email-service/newsletter-email-event-storage.js
        tests:
          - test/unit/server/services/email-service/sending-status.test.ts
          - test/unit/server/services/email-service/sending-status-service.test.ts
          - test/unit/server/services/email-service/email-service.test.js
          - test/unit/server/services/email-service/batch-sending-service.test.js
          - test/unit/server/services/email-service/mailgun-email-provider.test.js
          - test/unit/server/services/email-service/email-renderer.test.js
          - test/integration/services/email-service/resume-interrupted-sends.test.js
          - test/integration/services/email-service/newsletter-email-event-storage.test.js
    unverified: []
    blockers:
      - >-
        Add wrapper identity/initialization assertions: missing dependencies, repeated init,
        captured versus current sibling references, partial publication and retry after each
        acquisition boundary. Existing wrapper units only assert URL-helper dispatch.
      - >-
        Preserve the tested boot email-init, jobs-readiness and pending-recovery barriers;
        characterize sibling publication timing and actual recovery failure/late-work behavior.
        The VM harness substitutes dependency implementations, and the real-boot HTTP witness
        controls recovery and subsequent external effects rather than exercising provider work.
      - >-
        Characterize locale-listener ownership, overlapping language changes and rejected
        changes; cleanup never removes the listener. Verify template compilation rejection
        retention and whether any supported lifecycle can recover from it.
      - >-
        Verify whole-server shutdown through final parent status persistence and late work.
        Batch cleanup snapshots sendEmail promises before emailJob's final write; current
        gated units cover nested work, not that final write or work admitted after the snapshot.
      - >-
        Test same-process restart through the retained wrapper: re-arming after stop, cleanup
        registration on the new server, repeated init on one server and restart while earlier
        work is pending. Captured consumers remain attached to the original graph. Accounting
        integration reconstructs a sender; existing wrapper units only assert URL-helper calls.
      - >-
        Preserve uncertainty at provider acceptance/database persistence and recovery boundaries;
        no tested cross-process ownership fence or exactly-once guarantee exists. Verify SDK
        deadline/socket settlement and DB failure behavior before changing shutdown ownership;
        the delivery wrapper currently provides no provider-request cancellation handle.
      - >-
        Characterize immutable-config replacement against captured helpers, concurrency/retry
        policy and queued/rendered snapshots. In particular, prepared recipients and cached
        bodies have different lifetimes from call-time provider/settings reads.
      - >-
        Retain analytics deep-export obligations: metric/provider-cache ownership, partial
        flush failure, concurrent pending-map changes and unawaited subscriber work. The
        delivery wrapper's cleanup does not own these separately composed objects.
      - >-
        Source-only recovery edge: stale and fresh queries use strict less-than/greater-than
        around one cutoff, so an exactly equal created_at matches neither query. This has
        not been reproduced and is not covered by the filter-string unit assertions.
  email-suppression-list:
    kind: directory
    disposition: migrate
    status: audited
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: complex
      modelImpact: fits
      reason: >-
        Move eager provider composition and two domain-event subscriptions into one owned
        startup path. Members and analytics currently capture the eager instance before its
        init, so preserve dependency order and adapt those consumers. Repeated init
        duplicates handlers; introduce idempotent registration and partial-start cleanup
        while retaining suppression failure behavior. A single retained instance fits once
        those subscription responsibilities are explicit.
      reviewedAt: f276513ba32b7114fd5cc4f7b02d3f82c0c31e3e
      sources:
        - core/server/services/email-suppression-list/service.js
        - core/server/services/email-suppression-list/mailgun-email-suppression-list.js
        - core/server/services/members/api.js
        - core/server/services/email-analytics/index.ts
        - core/boot.js
      dependencies:
        complexity: straightforward
        reason: >-
          The provider already takes a Suppression model and Mailgun client, but init replaces the
          injected model from a global import and directly binds shared email events, DomainEvents
          and logging. These are bounded root-wiring changes. Members and Email Analytics currently
          capture the eager root, so they must receive its ready instance after migration;
          duplicated subscriptions are a separate lifecycle problem.
        preparation:
          - >-
            Acquire the ready Suppression model once in the root, remove init's global replacement,
            and inject logging and the email-event subscription/dispatch capability.
          - Update Members and Email Analytics to consume the ready provider after its boot barrier.
          - >-
            Keep the shared suppression event/data constructors independent; handle their
            non-allowlisted assertion helper explicitly rather than hiding it through the root.
        reviewedAt: 44ad405bff4a8700af2ed51f833ebc08d25dddff
        sources:
          - core/server/services/email-suppression-list/service.js
          - core/server/services/email-suppression-list/mailgun-email-suppression-list.js
          - core/server/services/email-suppression-list/email-suppression-list.js
          - core/server/services/members/api.js
          - core/server/services/email-analytics/index.ts
          - core/boot.js
    entryPoints:
      - email-suppression-list/index.js
    notes: >-
      Preserve eager provider composition, suppression fallback behavior and domain-event
      subscription behavior.
    contracts:
      exports:
        summary: >-
          CommonJS root and service.js expose one MailgunEmailSuppressionList object with init,
          getSuppressionData, getBulkSuppressionData and three removal methods. Methods use their
          receiver; Suppression and apiClient are mutable public fields. The deep helper exports
          EmailSuppressionData and EmailSuppressedEvent; members imports the latter directly.
          Reads return suppressed/info with spam or fail reasons, and bulk results preserve
          input order with an empty-array shortcut. Root tests pin one instance shared by
          index.js and service.js. Provider tests pin single reads and the bulk contract: one
          interpolated filter with quotes left unescaped, input order, duplicates, case-sensitive
          matching and whole-result fallback on query or mapping failure.
        sources:
          - core/server/services/email-suppression-list/index.js
          - core/server/services/email-suppression-list/service.js
          - core/server/services/email-suppression-list/mailgun-email-suppression-list.js
          - core/server/services/email-suppression-list/email-suppression-list.js
          - core/server/services/members/members-api/members-api.js
        tests:
          - test/unit/server/services/email-suppression-list/email-suppression-list.test.ts
          - test/unit/server/services/email-suppression-list/index.test.ts
          - test/unit/server/services/email-suppression-list/mailgun-email-suppression-list.test.ts
          - test/e2e-api/members/member-projection.test.ts
      construction:
        summary: >-
          Module evaluation constructs one MailgunClient retaining config and settings accessors,
          then one suppression list retaining that client and models.Suppression. Constructors
          perform no provider requests or subscriptions; the SDK client is created later when
          a removal asks for it. No other runtime construction of the suppression list was
          found. init replaces even an injected Suppression field with the shared model.
          Root tests pin the eager composition and that evaluation subscribes to nothing;
          provider tests pin init's model replacement.
        sources:
          - core/server/services/email-suppression-list/service.js
          - core/server/services/email-suppression-list/mailgun-email-suppression-list.js
          - core/server/lib/mailgun/mailgun-client.js
        tests:
          - test/unit/server/services/email-suppression-list/index.test.ts
          - test/unit/server/services/email-suppression-list/mailgun-email-suppression-list.test.ts
      consumers:
        summary: >-
          Members retains the root in its BREAD and account services: reads/browse attach local
          suppression, add/email edits derive email_disabled, and account removal reenables mail.
          Admin removal resolves the root at request time and checks its boolean result. Boot
          injects it into newsletter analytics storage for provider complaint/unsubscribe cleanup.
          EmailEventProcessor dispatches bounce/complaint events; this root persists suppression
          then emits EmailSuppressedEvent, whose members subscriber disables the address.
          Integration tests cover this chain with fetched provider events stubbed and pin that
          Members retains the root object; the boot harness pins analytics receiving it before
          its init. HTTP tests cover local projection/removal; storage unit tests inject cleanup
          stubs, not the root.
        sources:
          - core/boot.js
          - core/server/services/members/api.js
          - core/server/services/members/members-api/members-api.js
          - core/server/services/members/members-api/services/member-bread-service.js
          - core/server/services/members/account-service.ts
          - core/server/api/endpoints/members-account.ts
          - core/server/api/endpoints/members.js
          - core/server/services/email-analytics/index.ts
          - core/server/services/email-service/newsletter-email-event-storage.js
          - core/server/services/email-service/email-event-processor.js
        tests:
          - test/integration/services/mailgun-email-suppression-list.test.js
          - test/unit/boot/init-services.test.ts
          - test/e2e-api/admin/members.test.js
          - test/e2e-api/members/member-projection.test.ts
          - test/e2e-api/members/session-auth.test.ts
          - test/unit/server/services/email-service/newsletter-email-event-storage.test.js
          - test/unit/server/services/members/members-api/services/members-bread-service.test.js
      startup:
        summary: >-
          Boot imports the root and calls init in the required service batch after core
          settings/models and Stripe. Members and newsletter analytics capture it earlier in
          that batch. init is async but contains no await: it refreshes the model and registers
          both subscriptions synchronously, then resolves undefined. Queries/removals have no
          readiness guard; only event handling requires init. Root tests pin reads and removal
          before init; booted integration tests exercise the resulting composition. Partial
          subscription failure is not characterized.
        sources:
          - core/boot.js
          - core/server/services/email-suppression-list/mailgun-email-suppression-list.js
          - core/server/services/members/service.js
          - core/server/services/email-analytics/index.ts
        tests:
          - test/integration/services/mailgun-email-suppression-list.test.js
          - test/unit/server/services/email-suppression-list/index.test.ts
          - test/unit/boot/init-services.test.ts
      lifetime:
        summary: >-
          Module caching and retained consumer references keep the same root and MailgunClient.
          Every init overwrites the model field and adds two fresh event handlers without a
          guard, unsubscribe or shutdown. Old handlers read this.Suppression when invoked, so
          model replacement affects them too. Persistence outlives the instance; duplicate rows
          retain their original reason/time but still cause a new suppression event. Provider
          tests pin two subscriptions per init, duplication on repeated init and handlers reading
          the current model; the duplicate-row integration case proves member disabling.
        sources:
          - core/server/services/email-suppression-list/service.js
          - core/server/services/email-suppression-list/mailgun-email-suppression-list.js
          - core/server/models/suppression.js
          - core/server/data/schema/schema.js
        tests:
          - test/integration/services/mailgun-email-suppression-list.test.js
          - test/unit/server/services/email-suppression-list/mailgun-email-suppression-list.test.ts
      configuration:
        summary: >-
          Reads use local suppression rows and need no provider config. MailgunClient rereads
          bulkEmail and settings accessors for each provider operation: any truthy config
          mailgun object wins; otherwise all three settings credentials must be present.
          getInstance constructs a fresh SDK client using baseUrl origin and timeout 60000;
          removal targets the primary domain. Accessor rereads can see replacement config
          trees, not in-place mutation or an established production reload facility. Client
          tests cover precedence and changed settings on fetchEvents. Root tests pin the root
          client rereading config and settings; provider tests pin removal against the primary
          domain. Configuration changes between the three awaited provider calls are untested.
        sources:
          - core/server/services/email-suppression-list/service.js
          - core/server/services/email-suppression-list/mailgun-email-suppression-list.js
          - core/server/lib/mailgun/mailgun-client.js
          - core/shared/config/validated.ts
        tests:
          - test/unit/server/lib/mailgun/mailgun-client.test.js
          - test/unit/server/services/email-suppression-list/index.test.ts
          - test/unit/server/services/email-suppression-list/mailgun-email-suppression-list.test.ts
      resources:
        summary: >-
          The root owns two shared DomainEvents subscriptions per init and borrows the shared
          model/database pool. Removal creates transient SDK clients and HTTP work; no root
          timer, transport handle, pending-work registry or disposal API exists. The installed
          SDK forwards the timeout to its HTTP client. DomainEvents dispatch does not await
          consumers and catches/logs rejected handlers; allSettled tracks test delivery only.
          Suppression persistence and member disabling are separate writes with no shared
          transaction. Integration tests explicitly settle events, not production shutdown.
        sources:
          - core/server/services/email-suppression-list/mailgun-email-suppression-list.js
          - core/server/lib/mailgun/mailgun-client.js
          - core/server/services/members/members-api/members-api.js
          - core/server/ghost-server.ts
          - package.json
        tests:
          - test/integration/services/mailgun-email-suppression-list.test.js
      failures:
        summary: >-
          Reads log query/mapping failures and return unsuppressed; one bulk failure defaults
          the whole result. Bounce handling accepts only integer 605/607; complaints always
          attempt a row. Duplicate-key errors still dispatch, while other insert failures log
          and stop. removeEmail awaits bounce, complaint and unsubscribe removal, then deletes
          the row; thrown failures return false. Provider false results are ignored, so failed
          remote cleanup can still yield local deletion and true. Individual cleanup methods
          return undefined after fulfilled calls, false after caught throws. Provider tests pin
          read fallbacks, integer bounce filtering (missing, string and fractional codes),
          duplicate-row redispatch for both duplicate codes, other insert failures, removal order
          and partial success, and the ignored false result through the real client for
          unconfigured and failed deletions. Integration covers accepted/ignored events and
          duplicate-row recovery; Admin's failure test stubs the root false result.
        sources:
          - core/server/services/email-suppression-list/mailgun-email-suppression-list.js
          - core/server/lib/mailgun/mailgun-client.js
          - core/server/services/members/account-service.ts
          - core/server/api/endpoints/members.js
        tests:
          - test/integration/services/mailgun-email-suppression-list.test.js
          - test/unit/server/services/email-suppression-list/mailgun-email-suppression-list.test.ts
          - test/e2e-api/admin/members.test.js
    unverified: []
    blockers:
      - >-
        Partial init (a failing second subscription) is not characterized. Each init duplicates
        listeners (pinned as incidental) and there is no unsubscribe or production drain. Pin
        concurrent event/removal behavior and separate persistence/member-write failures before
        introducing disposal or changing event ordering.
      - >-
        Resolve the provider-to-root failure contract: MailgunClient resolves false for
        unconfigured or failed deletion and removeEmail ignores it, reporting success (pinned
        through the real client). MemberAccountService also ignores a false root result and
        reenables mail, unlike the Admin endpoint. These need an explicit decision, not an
        accidental change hidden inside lifecycle migration.
      - >-
        Invalid read/bulk inputs (non-string or non-array addresses) are not characterized.
      - >-
        Pin configuration changes between the three provider removals, malformed provider
        configuration and the fallback domain (removal targets only the primary domain).
        Establish timeout, retry/redirect, sockets and pending-operation ownership for the
        installed SDK before promising safe shutdown.
      - >-
        Duplicate rows keeping their original reason/time, database failures beyond insert and
        logging failures are not characterized. No shared transaction makes provider removal,
        suppression deletion and member reenabling atomic.
  explore-ping:
    kind: directory
    disposition: migrate
    status: audited
    migrated: true
    auditedAt: c2999f4e13da6360bceced808bb52c4a04b5ad03
    migration:
      complexity: straightforward
      modelImpact: fits
      reason: >-
        At this revision each eligible init constructs an instance and starts an unawaited
        ping. Retaining the composition fits: boot should apply the environment gate, await
        member/email/stat prerequisites, then start ping separately without waiting for the
        request. Preserve payload behavior and explicitly handle detached failures; the
        instance has no timers or subscriptions requiring a new model.
      reviewedAt: f276513ba32b7114fd5cc4f7b02d3f82c0c31e3e
      sources:
        - core/server/services/explore-ping/index.ts
        - core/server/services/explore-ping/explore-ping-service.ts
        - core/server/services/posts/posts-service-instance.js
        - core/boot.js
        - test/e2e-server/boot-explore-ping.isolated.test.ts
      dependencies:
        complexity: straightforward
        reason: >-
          The implementation already receives all dependencies. The root unnecessarily constructs
          the full Posts graph for statistics, but that can be narrowed locally: Members exposes its
          statistics object before init, and MRR is deliberately read through an optional Stats
          facade. Preserve those narrow read paths rather than imposing whole-service readiness
          barriers.
        preparation:
          - >-
            Wire a narrow post-statistics collaborator instead of constructing PostsService,
            retaining Members' existing statistics object without requiring its API to initialize.
          - >-
            Keep MRR access optional and resolved when the payload is collected, preserving
            absence/failure behavior rather than introducing a Stats readiness requirement.
          - >-
            Preserve environment/privacy gates and nonblocking ping activation when migrating the
            root; dependency wiring does not require adding payload or availability logic to boot.
        reviewedAt: 44ad405bff4a8700af2ed51f833ebc08d25dddff
        sources:
          - core/server/services/explore-ping/index.ts
          - core/server/services/explore-ping/explore-ping-service.ts
          - core/server/services/posts/posts-service-instance.js
          - core/server/services/posts/stats/post-stats.js
          - core/boot.js
          - core/server/services/members/service.js
    entryPoints:
      - explore-ping/index.ts
    notes: >-
      Initializer adoption was rechecked against e716eb07c8c48a4dd8df0a45c6063e6a620c3157;
      auditedAt and the migration assessments retain their historical provenance. The prior
      root is recorded at https://github.com/TryGhost/Ghost/blob/c2999f4e13da6360bceced808bb52c4a04b5ad03/ghost/core/core/server/services/explore-ping/index.ts.
      Boot now awaits one retained instance. The factory constructs in every environment and
      starts the detached ping only in production and development, replacing the historical
      fresh instance per eligible init. The payload and nonblocking boot ping are unchanged;
      ownership of unfinished outbound work remains open.
    contracts:
      exports:
        summary: >-
          The TypeScript root default-exports a defineService root exposing init(): Promise<void>
          and a guarded service getter; CommonJS boot uses .default. The historical named
          createService and init exports are removed; no other consumer used them. There is no
          stop/reset method or extras. The implementation class and dependency type remain deep
          exports; only tests construct that class directly. Root units pin init resolving
          undefined on every call and the ping starting on the retained instance; reading
          service before init throws through the kernel helper.
        sources:
          - core/server/services/explore-ping/index.ts
          - core/server/services/explore-ping/explore-ping-service.ts
          - core/kernel/define-service.ts
          - core/boot.js
        tests:
          - test/unit/server/services/explore-ping/index.test.ts
          - test/unit/kernel/define-service.test.ts
      construction:
        summary: >-
          Import declares only the initializer. The first init loads shared config, settings,
          logging, version and request helpers, constructs a fresh posts service, including
          PostStats, PostsExporter and PostEmailHandler, and one ExplorePingService, then retains
          it; concurrent calls share initialization. Posts composition captures
          emailService.service, though this consumer only uses stats. Construction happens in
          every environment, even when ping is disabled or its URL is absent; only the ping is
          gated. Root units pin that test and staging environments do not ping, that eligible
          environments construct with a fresh Posts service and ping once, that repeated init
          retains one instance, and that a synchronous Posts construction error rejects init
          unchanged.
        sources:
          - core/server/services/explore-ping/index.ts
          - core/kernel/define-service.ts
          - core/server/services/posts/posts-service-instance.js
          - core/server/services/posts/posts-service.js
          - core/server/services/posts/posts-exporter.js
          - core/server/services/posts/post-email-handler.js
          - core/server/services/posts/stats/post-stats.js
        tests:
          - test/unit/server/services/explore-ping/index.test.ts
          - test/unit/kernel/define-service.test.ts
      consumers:
        summary: >-
          Boot awaits the initializer and is the only production root caller found; no request,
          job or CLI consumer or
          second composition path was found. Payload collection consumes posts.stats,
          members.stats and statsService.api.mrr. Missing MRR availability silently produces
          an empty array. Implementation tests inject these shapes. Root units wire the shared
          Members statistics object and Stats facade and pin the exact request body, reading MRR
          from a facade that becomes available during payload collection. The isolated boot case
          collects the payload through the real Posts, Members and Stats roots against the
          default database, including the real Stats API's zero-MRR currency entry.
        sources:
          - core/boot.js
          - core/server/services/explore-ping/index.ts
          - core/server/services/explore-ping/explore-ping-service.ts
          - core/server/services/members/service.js
          - core/server/services/stats/index.ts
        tests:
          - test/unit/server/services/explore-ping/index.test.ts
          - test/e2e-server/boot-explore-ping.isolated.test.ts
      startup:
        summary: >-
          Boot awaits init in its required service Promise.all after database/settings setup,
          before app mounting and readiness, including backend-only boot. The factory constructs
          in every environment and starts the ping only in production/development. Member stats
          already exists at import; boot awaits the Stats root before this Promise.all and the
          factory passes its ready aggregate. Init launches the ping synchronously and resolves
          without awaiting payload collection or HTTP completion. Root
          units apply the real configuration policy per environment, including development, and
          show init resolving while post statistics and the request are held pending. The
          isolated boot case opens the gate for this initializer only during a real boot:
          readiness is notified once and the Admin API answers while the Explore response is
          held. Boot's Promise.all propagation is covered generically, not per service.
        sources:
          - core/boot.js
          - core/shared/config/helpers.ts
          - core/server/services/explore-ping/index.ts
          - core/server/services/members/service.js
          - core/server/services/stats/index.ts
        tests:
          - test/unit/server/services/explore-ping/index.test.ts
          - test/e2e-server/boot-explore-ping.isolated.test.ts
          - test/unit/boot/init-services.test.ts
      lifetime:
        summary: >-
          Successful init retains one instance; repeated init reuses it without a second ping,
          replacing the historical fresh instance and independent overlapping pings per eligible
          init. No completion promise, success state or deduplication guard for the boot ping
          is retained; there is no implemented schedule or retry at this layer. The instance
          retains dependency references but reads their mutable properties during each ping.
          Server stop neither drains the ping nor prevents later calls. Root units pin that
          repeated init retains one instance and pings once; retained identity itself is covered
          by the kernel's initializer tests.
        sources:
          - core/server/services/explore-ping/index.ts
          - core/server/services/explore-ping/explore-ping-service.ts
          - core/kernel/define-service.ts
          - core/boot.js
          - core/server/ghost-server.ts
        tests:
          - test/unit/server/services/explore-ping/index.test.ts
          - test/unit/kernel/define-service.test.ts
      configuration:
        summary: >-
          Ping reads URL then enabled state once; payload reads version/site metadata before
          awaiting three post queries, growth permission afterwards, then strict-true Stripe
          live mode after member count. Disabled growth omits both growth fields; unavailable
          MRR or non-live Stripe yields an empty array. Post queries count published/sent posts;
          member count includes all members. Tests vary injected settings after construction,
          but do not establish changes during pending work or real-root configuration wiring.
        sources:
          - core/server/services/explore-ping/explore-ping-service.ts
          - core/server/services/posts/stats/post-stats.js
          - core/server/services/members/stats/members-stats.js
          - core/server/services/stats/mrr-stats-service.js
          - core/server/data/schema/default-settings/default-settings.json
        tests:
          - test/unit/server/services/explore-ping/explore-ping-service.test.ts
      resources:
        summary: >-
          The owned posts helpers acquire no separate connections or listeners on construction;
          their queries use shared database resources. Outbound work uses shared @tryghost/request.
          Explore stores no abort handle or pending-work registry, supplies no request timeout,
          and registers no boot cleanup. There are no service-owned timers or subscriptions;
          transport/DNS resources remain with the shared request dependency.
        sources:
          - core/server/services/explore-ping/index.ts
          - core/server/services/explore-ping/explore-ping-service.ts
          - core/server/services/posts/posts-service-instance.js
          - core/server/services/posts/stats/post-stats.js
          - core/server/services/posts/posts-exporter.js
          - core/server/services/posts/post-email-handler.js
          - core/boot.js
          - core/server/ghost-server.ts
          - package.json
        tests: []
      failures:
        summary: >-
          Factory failure rejects awaited init and can fail boot, leaving service unavailable
          until a later explicit init; ping rejection is detached.
          Missing URL warns, disabled ping skips. Any post-query/date failure nulls all post
          fields; member or MRR failure nulls both growth fields. Request rejection warns and
          fulfills, while payload setup or serialization errors can reject ping outside these
          catches and reach boot's unhandled-rejection logger. Implementation tests assert null
          fallback for post/member failure and request-error logging. Root units pin a request
          rejection warning while the detached ping fulfills, and a synchronous payload error
          rejecting the detached ping while init resolves. MRR failure and boot's handling of
          the detached rejection remain untested.
        sources:
          - core/server/services/explore-ping/index.ts
          - core/server/services/explore-ping/explore-ping-service.ts
          - core/boot.js
        tests:
          - test/unit/server/services/explore-ping/explore-ping-service.test.ts
          - test/unit/server/services/explore-ping/index.test.ts
    unverified: []
    blockers:
      - >-
        Root units pin environment gating through the real configuration policy, construction
        with a fresh Posts service in eligible environments, one retained instance across
        repeated init, construction failure rejecting init and init resolving before payload
        and request completion; the isolated boot case runs the real boot. Adoption constructs
        in every environment and gates only the ping, replacing fresh construction per init.
      - >-
        The isolated boot case now collects the payload through the real Posts, Members and
        Stats roots and database, including the Stats API's zero-MRR entry, and root units read
        the Stats facade when it becomes available during collection. Stats initialization
        ordering relative to this init remains source evidence.
      - >-
        Decide ownership of overlapping pings, unfinished queries/requests, cancellation and
        shutdown. No service drain exists, and the request has no configured timeout; current
        tests do not establish stalled-request, HTTP-status, redirect or transport cleanup behavior.
      - >-
        Add pending-work configuration contracts: URL/enabled state are checked before queries,
        growth permission and Stripe mode later. Setting changes do not cancel a launched ping.
        Cover real post/member count semantics and unavailable versus failed MRR distinctly.
      - >-
        Root units now pin a detached payload error rejecting ping while init resolves.
        Serialization failures, MRR rejection, post-date errors, sibling queries continuing
        after Promise.all rejects, warning details for statistics failures and boot's handling
        of the detached rejection remain uncharacterized.
  export-requests:
    kind: directory
    disposition: migrate
    status: audited
    migrated: true
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: straightforward
      modelImpact: fits
      reason: >-
        The boot-owned default root constructs one ExportRequestsService with explicit config,
        logging and request dependencies. The exports endpoint reads its ready instance per
        request. Configuration, signing and delivery stay per operation, retaining validation,
        timeout and zero-retry behavior without startup network work.
      reviewedAt: 44e8807320c51ab2b5cb67d3b50eafc252f72cb2
      sources:
        - core/server/services/export-requests/index.ts
        - core/boot.js
        - core/server/services/export-requests/export-requests-service.ts
        - core/server/api/endpoints/exports.js
        - core/server/lib/signed-webhook.ts
        - test/unit/server/services/export-requests/export-requests-service.test.ts
    entryPoints:
      - export-requests/index.ts
    notes: >-
      The prior auditedAt records the pre-migration baseline. This migration adds a boot-owned
      root and explicit dependencies; request signing, live configuration and zero retries are
      unchanged. Current root and boot contracts are recorded below; broader delivery and
      transport lifecycle questions remain in blockers.
    contracts:
      exports:
        summary: >-
          The TypeScript root default-exports a defineService initializer with init and a
          guarded service getter; there are no extras. The implementation exports its
          ExportRequestsService class, whose constructor requires config, logging and request.
          requestExport requires its receiver and resolves undefined after delivery; it does
          not return a job ID or inspect the response body. Class units use explicit dependencies;
          root and HTTP tests exercise the shared ready instance through the endpoint.
        sources:
          - core/server/services/export-requests/index.ts
          - core/server/services/export-requests/export-requests-service.ts
          - core/server/api/endpoints/exports.js
        tests:
          - test/unit/server/services/export-requests/export-requests-service.test.ts
          - test/e2e-api/admin/exports-request.test.ts
      construction:
        summary: >-
          Import declares the initializer without constructing an application instance. Boot
          awaits init, whose factory captures the shared config, logger and request objects
          without reading export settings or sending a request. The endpoint retains the root
          and reads service when handling a request. No other runtime constructor was found;
          tests construct isolated class instances with explicit dependencies. Root units load
          the real endpoint, observe calls on the ready instance, assert idle construction and
          use the retained endpoint with changed configuration.
        sources:
          - core/server/services/export-requests/index.ts
          - core/server/services/export-requests/export-requests-service.ts
          - core/server/api/endpoints/exports.js
        tests:
          - test/unit/server/services/export-requests/root.test.ts
      consumers:
        summary: >-
          POST /exports is the only runtime requestExport caller found. The endpoint validates
          known boolean components with at least one selected, normalizes all six keys and
          takes the requester from authenticated context. It awaits delivery before returning
          202. Routing applies Admin authentication and db exportContent permission without
          a feature flag; integration tokens are absent from the normal allowlist. GET archive
          downloads use a separate exporter. HTTP tests cover owner/admin success, unauthorized,
          editor, author and integration-token rejection, payload normalization, requester
          spoofing and invalid components. Staff-token behavior is source-only.
        sources:
          - core/server/api/endpoints/exports.js
          - core/server/data/exporter/export-components.ts
          - core/server/web/api/endpoints/admin/routes.js
          - core/server/web/api/endpoints/admin/middleware.js
          - core/server/api/endpoints/utils/permissions.js
        tests:
          - test/e2e-api/admin/exports-request.test.ts
          - test/unit/server/services/export-requests/root.test.ts
      startup:
        summary: >-
          Boot awaits the default root's init in initCore before frontend and API setup.
          The factory only captures dependencies; missing export configuration does not block
          boot. Consumers read service after readiness, and the kernel rejects earlier access.
          The HTTP suite boots Ghost and reaches the real endpoint; no separate service-specific
          boot harness is needed. Generic readiness/retry semantics are covered by kernel tests;
          service-specific injected construction failure and repeated actual boot are untested.
        sources:
          - core/server/services/export-requests/index.ts
          - core/boot.js
          - core/shared/express.js
          - core/server/web/parent/backend.js
          - core/server/web/api/app.js
          - core/server/web/api/endpoints/admin/app.js
          - core/server/web/api/endpoints/admin/routes.js
          - core/server/api/endpoints/index.js
          - core/server/services/export-requests/export-requests-service.ts
          - package.json
        tests:
          - test/unit/kernel/define-service.test.ts
          - test/e2e-api/admin/exports-request.test.ts
      lifetime:
        summary: >-
          The initializer retains one process-lifetime instance across successful init calls;
          the endpoint reads that same instance through the retained root. There is no reset,
          reinit or disposal hook and no mutable job state. Every request builds its own signed
          payload; overlapping or repeated calls can send independently with no deduplication,
          local persistence or completion tracking. Root tests observe shared instance use and
          hold endpoint delivery. Kernel tests protect generic retention; concurrent deliveries
          and repeated actual boot remain outside this service's coverage.
        sources:
          - core/server/services/export-requests/index.ts
          - core/server/services/export-requests/export-requests-service.ts
          - core/server/api/endpoints/exports.js
          - core/boot.js
          - core/server/ghost-server.ts
        tests:
          - test/unit/kernel/define-service.test.ts
          - test/unit/server/services/export-requests/root.test.ts
      configuration:
        summary: >-
          Each call reads webhook URL, signing secret and site ID through the retained config
          object's get method, then captures those values for that request. URL and secret
          require nonempty strings; string/number site IDs are normalized to a nonempty string.
          There is no captured configuration subtree. Later get calls observe a replacement
          tree, but config set/reset is test support rather than production hot reload.
          Public config omits the signing secret. Tests cover initial values, numeric IDs,
          missing settings and secret omission. Root units additionally replace configuration
          between requests through one retained endpoint and verify each signed payload.
        sources:
          - core/server/services/export-requests/export-requests-service.ts
          - core/shared/config/validated.ts
          - core/server/api/endpoints/utils/public-config/config.js
        tests:
          - test/unit/server/services/export-requests/export-requests-service.test.ts
          - test/e2e-api/admin/exports-request.test.ts
          - test/unit/server/services/export-requests/root.test.ts
      resources:
        summary: >-
          The service creates no database record, worker or own timer. It awaits the shared
          request dependency with a 30-second request-timeout option and retry limit zero.
          Installed dependency source shows shared DNS cache/timers and transport-owned
          requests; this root exposes no cancellation or disposal. HTTP shutdown bounds the
          incoming request drain but does not pass cancellation to the outbound delivery.
          Unit tests assert retry/timeout options; root tests hold delivery and assert the
          endpoint awaits it. HTTP tests intercept outbound transport. Actual timeout,
          redirects, DNS/socket cleanup and pending-delivery shutdown are not exercised.
        sources:
          - core/server/services/export-requests/export-requests-service.ts
          - core/server/lib/signed-webhook.ts
          - core/server/api/endpoints/exports.js
          - core/server/ghost-server.ts
          - package.json
        tests:
          - test/unit/server/services/export-requests/export-requests-service.test.ts
          - test/e2e-api/admin/exports-request.test.ts
          - test/unit/server/services/export-requests/root.test.ts
      failures:
        summary: >-
          Missing URL rejects with NotFoundError; missing secret/site ID logs and rejects with
          IncorrectUsageError. Rejected delivery logs the transport message and becomes a
          generic status-502 InternalServerError. Config reads, signing/serialization and
          pre-send logging are outside that catch; logger failure can also replace the intended
          error. A resolved request supplies no confirmation of archive generation or
          compensation for ambiguous delivery. Unit and HTTP tests prove configuration errors,
          the signed wire payload and failed-delivery mapping; pre-send exceptions, real
          timeout behavior and remote completion are outside their coverage.
        sources:
          - core/server/services/export-requests/export-requests-service.ts
          - core/server/lib/signed-webhook.ts
          - core/server/api/endpoints/exports.js
        tests:
          - test/unit/server/services/export-requests/export-requests-service.test.ts
          - test/e2e-api/admin/exports-request.test.ts
          - test/unit/server/services/export-requests/root.test.ts
    unverified: []
    blockers:
      - >-
        Root units observe the ready instance through the retained endpoint and pin idle
        construction and delivery. The HTTP suite exercises boot-owned readiness through the
        real route. Repeated actual boot and service-specific construction-failure injection
        remain outside this coverage; do not add reset/replacement behavior without tests.
      - >-
        Root tests now pin same-instance configuration replacement between completed requests.
        Changes during pending delivery and production hot reload remain separate questions.
        Pin staff-token authorization separately.
      - >-
        Establish pending-delivery and shared transport ownership before adding disposal.
        Retry limit zero is asserted, but actual timeout, redirect and shutdown behavior is
        not. The installed transport follows redirects; zero retries alone is not proof of
        one outbound request or safe cancellation after a lost response.
      - >-
        Characterize overlapping requests, ambiguous delivery and pre-send config/signing/logging
        failures without treating local 202 as completed export generation. There is no local
        job persistence, idempotency or compensation; the host's behavior is outside this
        repository and intercepted HTTP tests cannot prove remote completion.
  gift-links:
    kind: directory
    disposition: migrate
    status: audited
    migrated: true
    auditedAt: c2999f4e13da6360bceced808bb52c4a04b5ad03
    migration:
      complexity: straightforward
      modelImpact: fits
      reason: >-
        Already constructs one graph after the database is ready and retains it. Wrap that
        factory, await boot initialization and switch the two API consumers from the live
        named export to the ready instance. Keep database transactions and token history in
        the implementation; startup acquires no independent resources.
      reviewedAt: f276513ba32b7114fd5cc4f7b02d3f82c0c31e3e
      sources:
        - core/server/services/gift-links/index.ts
        - core/server/services/gift-links/service.ts
        - core/boot.js
        - core/server/api/endpoints/utils/gift-link-access.ts
        - test/unit/server/services/gift-links/index.test.ts
      dependencies:
        complexity: straightforward
        reason: >-
          Knex and action recording are already injected and acquired after the database connects.
          Logging in the action helper and case-key conversion in the codec/serializer are the
          remaining boundary violations. The data shape is small, so these can be removed or wired
          without changing transactions or introducing service dependencies.
        preparation:
          - >-
            Supply logging to the action recorder through the root while preserving best-effort
            action failures.
          - >-
            Use explicit field mapping for the small codec/serializer shapes, or separately review
            an exact-file case-keys allowance; do not make importable serializers depend on an
            initialized service.
        reviewedAt: 44ad405bff4a8700af2ed51f833ebc08d25dddff
        sources:
          - core/server/services/gift-links/index.ts
          - core/server/services/gift-links/actions.ts
          - core/server/services/gift-links/codec.ts
          - core/server/services/gift-links/serializers.ts
          - core/server/services/gift-links/service.ts
          - core/server/api/endpoints/utils/serializers/output/gift-links.ts
    entryPoints:
      - gift-links/index.ts
    notes: >-
      Migration rechecked against base e7ab0fe5a8406036cfb0d790e8db7afdb58bfe36; the original
      audit and dependency-assessment revisions are retained above. The root now supplies action
      logging; case-key and date codecs use the existing exact-file library allowances. Deferred
      database binding, token history and public access/cache policy are unchanged. No extras.
    contracts:
      exports:
        summary: >-
          The TypeScript root default-exports defineService with async init and a guarded service
          getter; RequestContext remains type-only. Reading service before readiness throws,
          and awaited receiver-independent init resolves undefined.
          The deep class requires its receiver: getPost/ensure/create return a post id plus
          giftLinks, getPostByToken returns that shape or null, and removeAll returns a number.
          Deep serializers map token/createdAt to gift_links token/created_at and removal count
          to meta.count. Admin tests assert list/field allowlists, replacement and count shape.
          Adapted root tests pin guarded pre-init service access, awaited init's return and
          retained binding identity; detached business-method calls remain untested.
        sources:
          - core/server/services/gift-links/index.ts
          - core/kernel/define-service.ts
          - core/server/services/gift-links/service.ts
          - core/server/services/gift-links/models.ts
          - core/server/services/gift-links/serializers.ts
          - core/server/api/endpoints/utils/serializers/output/gift-links.ts
        tests:
          - test/e2e-api/admin/gift-links.test.ts
          - test/integration/services/gift-links.test.ts
          - test/unit/server/services/gift-links/index.test.ts
      construction:
        summary: >-
          Import declares the initializer without constructing a service. Its factory reads the
          shared database knex and models, partially applies the best-effort action recorder with
          root-supplied logging and returns one GiftLinksService retaining knex and the callback.
          The recorder still reads models.Action per invocation. Later init calls preserve it.
          No separate production deep construction was found. The constructor validates neither
          dependency nor database readiness; an early init can publish an unusable captured
          dependency. Integration tests construct the class with real model knex and matching
          recorder wiring. Root tests assert deferred acquisition and retained knex/instance across
          init calls and exercise earlier public/Admin consumers.
        sources:
          - core/server/services/gift-links/index.ts
          - core/server/services/gift-links/service.ts
          - core/server/services/gift-links/actions.ts
          - core/server/data/db/index.js
          - core/server/data/db/connection.ts
        tests:
          - test/integration/services/gift-links.test.ts
          - test/unit/server/services/gift-links/index.test.ts
      consumers:
        summary: >-
          Boot awaits the root. Admin posts/pages gift-link endpoints retain the initializer and
          read its ready service for browse/ensure/create and site-wide removal, with permission
          checks and actor mapping
          in the endpoint. Public posts/pages reads use gift-link-access to resolve internal
          context before the posts response cache, then apply a paid-member shim for the matched post.
          Pages currently has no response cache but resolves the same key data.
          Frontend entry lookup supplies that context; HTTP Content API gift query parameters
          do not. Verified frontend renders set no-store/noindex/no-referrer and feed toast and
          analytics helpers without exposing a member. Real HTTP tests pin access/redirects,
          response headers, toast overrides and rendered analytics attributes, not analytics
          ingestion. Admin role tests cover Owner success and Author/Contributor browse denial,
          not every mutation/role or integration actor path.
        sources:
          - core/boot.js
          - core/server/api/endpoints/gift-links.ts
          - core/server/web/api/endpoints/admin/routes.js
          - core/server/api/endpoints/posts-public.js
          - core/server/api/endpoints/pages-public.js
          - core/server/api/endpoints/utils/gift-link-access.ts
          - core/frontend/services/data/entry-lookup.js
          - core/frontend/services/routing/controllers/entry.ts
          - core/frontend/services/routing/controllers/entry/gift-links.ts
          - core/frontend/web/middleware/frontend-caching.js
          - core/frontend/helpers/ghost_head.js
          - core/frontend/helpers/ghost_foot.js
        tests:
          - test/e2e-api/admin/gift-links.test.ts
          - test/e2e-api/content/gift-links.test.ts
          - test/e2e-frontend/gift-links.test.ts
          - test/e2e-frontend/gift-links-toast-override.test.ts
          - test/unit/api/endpoints/utils/gift-link-access.test.ts
          - test/integration/services/gift-links.test.ts
          - test/unit/server/services/gift-links/index.test.ts
      startup:
        summary: >-
          initCore awaits init after database setup, settings, signing keys and i18n, before
          members-metafields and request serving. The factory is synchronous, but the shared
          initializer publishes only after its promise settles; consumers use the guarded getter.
          Gift-access key resolution additionally needs
          the live-link tables, while the paid-member shim reads Product models. Root and deep
          codec/serializer imports do not need service initialization. Frontend/Admin suites
          exercise booted composition. Adapted root tests pin deferred database acquisition,
          guarded pre-init access and publication after awaited init; failed/early init and a
          real repeated boot with a different database instance remain untested.
        sources:
          - core/boot.js
          - core/server/services/gift-links/index.ts
          - core/server/api/endpoints/gift-links.ts
          - core/server/api/endpoints/utils/gift-link-access.ts
          - core/server/services/members/create-paid-member-shim.ts
        tests:
          - test/e2e-api/admin/gift-links.test.ts
          - test/e2e-frontend/gift-links.test.ts
          - test/unit/server/services/gift-links/index.test.ts
      lifetime:
        summary: >-
          Service and captured knex survive repeated init; no reset/stop/rebinding exists. Reads
          query persisted state without a service cache. Mint inserts token history and upserts
          one live association in a transaction; create replaces access while keeping history,
          ensure returns an existing link or mints after a separate read, and removeAll deletes
          every live association but keeps history. Unique post_id enforces at most one live
          link, not concurrent ensure returning the same token. Tests pin sequential ensure,
          replaced-token rejection, removal scope/count and uniqueness. Concurrent Admin ensures
          assert one surviving returned token, without forcing both reads before mint or pinning
          which caller wins. Public key units stub token lookup; actual cached revocation is untested.
        sources:
          - core/server/services/gift-links/index.ts
          - core/server/services/gift-links/service.ts
          - core/server/data/schema/schema.js
          - core/server/api/endpoints/utils/gift-link-access.ts
          - core/server/api/endpoints/posts-public.js
        tests:
          - test/integration/services/gift-links.test.ts
          - test/e2e-api/admin/gift-links.test.ts
          - test/unit/api/endpoints/utils/gift-link-access.test.ts
          - test/unit/server/services/gift-links/index.test.ts
      configuration:
        summary: >-
          The root/service read no feature config, settings or URL values. Database config is
          captured by the borrowed connection; tokens use 24 random bytes encoded base64url and
          creation time uses Date. No expiry, use count, visibility gate or automatic visibility
          revocation exists here. API permissions and frontend format/cache rules are consumer
          policy. Public key generation distinguishes no token, unresolved token and resolved
          post id; the latter reuses a cache variant across replacement tokens. URL-based toast
          assets use the separately frozen shared URL helpers, while branding/analytics settings
          are consumer reads. Tests pin URL-safe tokens, decoded minimum byte length and sampled
          uniqueness, not entropy mathematically; config test replacement is not hot reload evidence.
        sources:
          - core/server/services/gift-links/index.ts
          - core/server/services/gift-links/service.ts
          - core/server/services/gift-links/models.ts
          - core/server/services/gift-links/codec.ts
          - core/server/lib/db-types/date.ts
          - core/server/data/db/connection.ts
          - core/server/api/endpoints/gift-links.ts
          - core/server/api/endpoints/utils/gift-link-access.ts
          - core/frontend/services/routing/controllers/entry/markdown.ts
          - core/frontend/helpers/ghost_head.js
          - core/frontend/helpers/ghost_foot.js
          - core/shared/url-utils.ts
        tests:
          - test/unit/server/services/gift-links/gift-link-token.test.ts
          - test/unit/api/endpoints/utils/gift-link-access.test.ts
          - test/e2e-frontend/gift-links.test.ts
      resources:
        summary: >-
          No owned client, timer, worker, listener or memory cache. The service borrows the shared
          knex pool and owns per-mint transaction work; action history is a separate awaited model
          write after the command's database change. Database rows carry token history/live state,
          with foreign-key cascade deletion and no scheduled pruning here. The public response cache
          belongs to posts-public, and rendering/analytics belong to frontend consumers. There is no
          root drain or cleanup registration; shutdown depends on shared HTTP/database ownership.
          Real DB tests enforce live uniqueness, removal and rollback after a failed live-
          association write; deletion cascades and in-flight shutdown remain untested.
        sources:
          - core/server/services/gift-links/service.ts
          - core/server/services/gift-links/actions.ts
          - core/server/data/db/connection.ts
          - core/server/data/schema/schema.js
          - core/server/models/action.js
          - core/server/services/posts-public/index.ts
          - core/server/ghost-server.ts
          - core/boot.js
        tests:
          - test/integration/services/gift-links.test.ts
          - test/e2e-api/admin/gift-links.test.ts
          - test/unit/server/services/gift-links/index.test.ts
      failures:
        summary: >-
          Missing posts throw NotFound; unknown or no-longer-live tokens return null. Query, codec,
          random generation and transaction errors propagate, without retries. Factory rejection
          leaves service unavailable and boot fails; the shared initializer permits an explicit
          retry, without service-specific failure/retry coverage. Action writes run
          after persistence and the production recorder logs/swallows failures, skipping writes when
          actor is absent. That boundary is enforced by action units and a real-DB deep-service test
          with a failing recorder. Gift access refuses missing key-resolution setup and rejects
          mismatched tokens; frontend strips invalid gifts with a 301 while other failures reach
          normal error handling. Product lookup failure logs and grants a paid shim with no tiers.
          Units pin those helpers and root query-error propagation. Real DB tests pin mint rollback
          and old-token retention for a failing live write, including retry; delete/codec failures
          and cache-enabled revoked-token responses remain untested.
        sources:
          - core/kernel/define-service.ts
          - core/boot.js
          - core/server/services/gift-links/service.ts
          - core/server/services/gift-links/actions.ts
          - core/server/services/gift-links/codec.ts
          - core/server/services/gift-links/models.ts
          - core/server/api/endpoints/utils/gift-link-access.ts
          - core/frontend/services/routing/controllers/entry.ts
          - core/frontend/services/routing/controllers/entry/gift-links.ts
          - core/server/services/members/create-paid-member-shim.ts
        tests:
          - test/unit/server/services/gift-links/actions.test.ts
          - test/integration/services/gift-links.test.ts
          - test/e2e-api/admin/gift-links.test.ts
          - test/unit/api/endpoints/utils/gift-link-access.test.ts
          - test/unit/server/services/members/create-paid-member-shim.test.ts
          - test/e2e-frontend/gift-links.test.ts
          - test/unit/server/services/gift-links/index.test.ts
    unverified: []
    blockers:
      - >-
        Characterize early or failed init and a real repeated boot with changed shared dependencies.
        Root tests already assert deferred database acquisition, retained captured knex, earlier
        public/Admin consumers and propagated query errors; they do not establish recovery from init
        before DB readiness.
      - >-
        Characterize overlapping ensure/create/removeAll, token collision and history retention.
        Real-database tests now force the live-association write to fail for ensure/create and
        assert history rollback, old-token retention, no action write and a successful retry.
        Concurrent HTTP ensures still do not force both reads before mint or guarantee that all
        callers retain access.
      - >-
        Exercise real public cache population and token replacement/removal together, proving
        revoked/wrong-entry reads cannot use unlocked or anonymous cached responses. Current key
        tests stub lookup and do not drive a cache; public HTTP gift tests run without that cache.
      - >-
        Complete consumer permissions and failure coverage for mutation denials, integration actors,
        delete/codec failures and shutdown with in-flight commands. Keep business access policy in
        scope when migrating: permission checks, gift verification and the paid-member shim are
        currently wired through API support, not all contained in this root.
  gifts:
    kind: directory
    disposition: migrate
    status: audited
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: complex
      modelImpact: fits
      reason: >-
        The gift, delivery and controller composition fits one retained facade: member, tier
        and staff dependencies are deferred until operations, and recovery already runs
        after boot. Preserve those getters instead of reading unready services during
        construction. Four domain subscriptions and partial publication need owned cleanup
        before retry, but the source does not establish an initializer cycle.
      reviewedAt: f276513ba32b7114fd5cc4f7b02d3f82c0c31e3e
      sources:
        - core/server/services/gifts/index.ts
        - core/server/services/stripe/service.js
        - core/server/services/members/api.js
        - core/boot.js
        - test/unit/boot/init-services.test.ts
      dependencies:
        complexity: complex
        reason: >-
          The root composes repositories, payment and email adapters, two schedulers, delivery and
          jobs. Its Members, Staff and Stripe getters intentionally defer reciprocal service access
          until operations, while recovery runs after those services are ready. Logging, event
          dispatch, renderer I/O, query/codec helpers and Luxon still enter below the root; Luxon
          also constructs domain dates outside service instances. Preserving those boundaries needs
          preparation across several layers, not just moving imports.
        preparation:
          - >-
            Retain explicit call-time payment/member/staff capabilities and after-boot recovery;
            test that constructing Gifts does not dereference those unready services.
          - >-
            Inject logging, domain dispatch, rendering I/O and query helpers into their existing
            collaborators, including the nested jobs module; keep delivery and scheduling owners
            explicit.
          - >-
            Characterize timezone/date arithmetic before choosing a narrow clock capability for
            Luxon-dependent domain operations; the existing Moment compatibility export is not a
            drop-in replacement. Review codec helper policy separately.
        reviewedAt: 44ad405bff4a8700af2ed51f833ebc08d25dddff
        sources:
          - core/server/services/gifts/index.ts
          - core/server/services/gifts/gift-service.ts
          - core/server/services/gifts/gift.ts
          - core/server/services/gifts/gift-delivery-service.ts
          - core/server/services/gifts/gift-email-renderer.ts
          - core/server/services/gifts/jobs/index.js
          - core/server/services/stripe/stripe-service.js
          - core/boot.js
    entryPoints:
      - gifts/index.ts
      - gifts/jobs/index.js
    notes: >-
      Preserve the gift, delivery and controller composition together with recurring jobs,
      signed callbacks and payment/member consumers. Record durable state separately from
      process state and distinguish mail transport acceptance from provider delivery outcomes.
    contracts:
      exports:
        summary: >-
          Named TypeScript exports are synchronous init(options), asynchronous
          recoverPendingDeliveries(), and initially undefined service, deliveryService and
          controller bindings; there is no default export, reset or shutdown. Init returns
          undefined and recovery before init resolves without work. The separate CommonJS jobs
          module exports scheduleGiftCleanupJob and scheduleGiftReminderJob. Implementations
          expose checkout, redemption, continuation, lifecycle, presentation and event capabilities;
          controller methods accept API frames and retain their GiftService. Class tests assert
          selected DTOs and controller forwarding/authentication, not the real root export shape.
        sources:
          - core/server/services/gifts/index.ts
          - core/server/services/gifts/jobs/index.js
          - core/server/services/gifts/gift-service.ts
          - core/server/services/gifts/gift-delivery-service.ts
          - core/server/services/gifts/gift-controller.ts
        tests:
          - test/unit/server/services/gifts/gift-service-interface.test.ts
          - test/unit/server/services/gifts/gift-controller.test.ts
      construction:
        summary: >-
          First init composes Gift/Delivery Bookshelf repositories over shared models/knex,
          checkout adapter, GhostMailer transport, MailgunClient, email service with its private
          Handlebars renderer, two SignedFlushSchedulers, delivery service, gift service and
          controller. Each scheduler registers itself during construction. Members/staff API
          dependencies and Stripe API resolve through getters; tiers and settings are retained
          facades. These production implementations are constructed only here, with the renderer
          constructed by the email service. Classes/codecs are test deep imports; events, job
          definitions and the analytics tag also have production deep consumers. No root
          construction-count or partial-construction rollback assertion was found.
        sources:
          - core/server/services/gifts/index.ts
          - core/server/services/gifts/gift-checkout-adapter.js
          - core/server/services/gifts/gift-email-service.ts
          - core/server/services/gifts/gift-email-renderer.ts
          - core/server/adapters/scheduling/signed-flush-scheduler.ts
          - core/server/lib/mail/ghost-mailer.js
          - core/server/lib/mailgun/mailgun-client.js
        tests: []
      consumers:
        summary: >-
          Members checkout/continuation, magic-link redemption, subscription presentation,
          member-event browsing and import reassignment resolve the root service at use time;
          Stripe completion/refund handlers likewise use getters. Members endpoints resolve
          controller and preview handlers resolve service. Job handlers retain GiftService;
          email analytics retains GiftDeliveryService for outcomes; four event handlers close
          over the original implementations. Admin signed flush endpoints dispatch deep events
          and return before work finishes. HTTP tests exercise checkout/webhook persistence,
          redemption and a two-request redemption race with Stripe/mail mocked. Signed flush
          HTTP tests stub dispatch and assert authentication/204/event type, not delivery.
        sources:
          - core/server/services/members/api.js
          - core/server/services/members/service.js
          - core/server/services/members/members-api/members-api.js
          - core/server/services/members/members-api/controllers/router-controller.js
          - core/server/services/members/members-api/services/member-bread-service.js
          - core/server/services/members/members-api/repositories/event-repository.js
          - core/server/services/members/import-export/index.ts
          - core/server/services/members/import-export/import/importer.ts
          - core/server/services/stripe/service.js
          - core/server/services/stripe/stripe-service.js
          - core/server/services/stripe/services/webhook/checkout-session-event-service.js
          - core/server/services/stripe/services/webhook/charge-refunded-event-service.js
          - core/server/api/endpoints/gifts-members.js
          - core/server/api/endpoints/gifts.js
          - core/server/web/gift-preview/controller.js
          - core/server/services/jobs-service/register-job-handlers.ts
          - core/server/services/email-analytics/index.ts
          - core/server/services/email-analytics/gift-email-analytics-batch-processor.ts
          - core/server/services/gifts/index.ts
        tests:
          - test/e2e-api/members/gift-subscriptions.test.js
          - test/e2e-api/admin/gift-reminders.test.js
          - test/e2e-api/admin/gift-deliveries.test.ts
      startup:
        summary: >-
          Boot runs the scheduling adapter, awaits Stripe, then calls gifts init with the Stripe
          Checkout config's checkout service before members, tiers and staff initialization;
          their facades/getters defer use. It passes initialized
          delivery/service instances into analytics and job registration before starting jobs.
          A boot harness holds Stripe unresolved and asserts gifts and jobs have not started,
          using module fakes. Outside test environments background boot starts unawaited delivery
          recovery, then independently catches recurring cleanup/reminder scheduling failures.
          Recovery optionally rebuilds delivery callbacks using captured rescheduleOnBoot and
          always attempts due/stale deliveries; it does not rebuild reminder callbacks. Full
          boot consumer tests do not exercise this production-only recovery branch or failure.
        sources:
          - core/boot.js
          - core/server/services/gifts/index.ts
          - core/server/services/gifts/jobs/index.js
          - core/server/services/jobs-service/register-job-handlers.ts
        tests:
          - test/unit/boot/init-services.test.ts
      lifetime:
        summary: >-
          The service guard preserves the first graph/options and four subscriptions. Bindings
          are published before controller/subscriptions finish, so a later init can skip an
          incomplete graph. Scheduler sets, renderer template/partial promises including rejected
          promises, mail transport and recurring-job flags survive same-process stop. Flags are
          set only after successful scheduling, without an in-flight guard. Job tests assert
          sequential scheduling once, test skipping and cron hour shape; they do not exercise
          concurrent calls, failed scheduling or backend replacement. Durable gifts/deliveries
          survive separately: real database tests assert single winners for concurrent pending
          and stale claims and sequential outcome precedence, not process restart or lease renewal.
        sources:
          - core/server/services/gifts/index.ts
          - core/server/services/gifts/jobs/index.js
          - core/server/services/gifts/gift-email-renderer.ts
          - core/server/services/gifts/gift-delivery-bookshelf-repository.ts
          - core/server/adapters/scheduling/signed-flush-scheduler.ts
          - core/server/ghost-server.ts
        tests:
          - test/unit/server/services/gifts/jobs/schedule-gift-cleanup.test.ts
          - test/unit/server/services/gifts/jobs/schedule-gift-reminders.test.ts
          - test/integration/services/gifts/gift-delivery-bookshelf-repository.test.ts
      configuration:
        summary: >-
          API URL, scheduling adapter/internal-key facade and rescheduleOnBoot are captured at
          init. Mail transport captures config then; email rendering reads settings, URL/from/
          reply-to/translation helpers per send and the bulk client resolves config/settings per
          call. The retained shared URL facade freezes base URLs. Config is immutable; injected
          test getters do not establish production hot reload.
          There is no root feature gate: checkout's member caller checks paid-members enablement,
          then GiftService validates tier, duration and Portal plans, preserving the cadence-only
          plan-gate exception. Settings supply timezone with UTC fallback; delivery is immediate
          today or 09:00 site time within 365 days. Unit fakes assert selected plan/date cases,
          mail tags, content, sender and transport selection; they do not prove root config capture.
        sources:
          - core/server/services/gifts/index.ts
          - core/server/services/gifts/gift-service.ts
          - core/server/services/gifts/gift-checkout-offer.ts
          - core/server/services/gifts/gift-date.ts
          - core/server/services/gifts/constants.ts
          - core/server/services/gifts/gift-email-service.ts
          - core/server/lib/mail/ghost-mailer.js
          - core/server/lib/mailgun/mailgun-client.js
          - core/server/services/members/members-api/controllers/router-controller.js
          - core/shared/config/loader.ts
          - core/shared/url-utils.ts
        tests:
          - test/unit/server/services/gifts/gift-service-interface.test.ts
          - test/unit/server/services/gifts/gift-email-service.test.ts
      resources:
        summary: >-
          Gifts owns its graph, mail transport, template promises, scheduler registrations/sets
          and four domain subscriptions; it borrows database, event bus, Stripe, settings, tiers,
          members/staff and scheduling/jobs backends. The bulk wrapper creates an SDK client per
          use with a 60-second timeout. Signed callbacks use current internal keys and a one-second
          offset; production error capture reports adapter failures without propagating them.
          Delivery recovery uses up to ten concurrent sends per invocation, batches of 1000 and
          at most 100 batches; unit fakes assert the concurrency bound and multi-batch continuation. Jobs own
          recurring execution, but gifts supplies no stop-claiming, drain, unregister or mail-close
          hook. DomainEvents allSettled tracks test work only and is not a production drain.
        sources:
          - core/server/services/gifts/index.ts
          - core/server/services/gifts/gift-delivery-service.ts
          - core/server/services/gifts/gift-email-renderer.ts
          - core/server/lib/mail/ghost-mailer.js
          - core/server/lib/mailgun/mailgun-client.js
          - core/server/adapters/scheduling/signed-flush-scheduler.ts
          - core/server/adapters/scheduling/error-capture.ts
          - core/server/adapters/scheduling/scheduling-default.ts
          - core/server/services/jobs-service/register-job-handlers.ts
          - core/server/ghost-server.ts
          - package.json
        tests:
          - test/unit/server/services/gifts/gift-delivery-service.test.ts
          - test/unit/server/adapters/scheduling/signed-flush-scheduler.test.ts
      failures:
        summary: >-
          Checkout persists gift/delivery before Stripe and attempts pending-row deletion on
          session failure; later session-binding failure has no compensation. Completion commits
          before best-effort dispatch/staff/buyer notices. Redemption updates member/gift/delivery
          and automation in a transaction; external transactions defer notification through an
          unreturned executionPromise continuation. Reminders commit their marker before mail,
          count per-gift failures and propagate poll failure; cleanup catches each phase and
          continues. Delivery claims pending or hour-stale sending rows, records acceptance with
          one persistence retry, and excludes failed/cancelled rows from recovery. Best-effort
          buyer/analytics failures do not undo acceptance. Unit fakes assert selected failure
          results/calls, not SQL rollback. Booted job tests assert expiry, reminder mail/markers,
          repeated-run suppression and injected failure logs; event integration asserts durable
          acceptance/cancellation with mail mocked, not exactly-once external delivery.
        sources:
          - core/server/services/gifts/gift-service.ts
          - core/server/services/gifts/gift-delivery-service.ts
          - core/server/services/gifts/gift-delivery-bookshelf-repository.ts
          - core/server/services/gifts/gift-bookshelf-repository.ts
          - core/server/services/gifts/gift-email-service.ts
          - core/server/services/gifts/index.ts
          - core/server/data/schema/schema.js
        tests:
          - test/unit/server/services/gifts/gift-service-interface.test.ts
          - test/unit/server/services/gifts/gift-service.test.ts
          - test/unit/server/services/gifts/gift-delivery-service.test.ts
          - test/integration/services/gifts/clean-gifts.test.ts
          - test/integration/services/gifts/send-gift-reminders.test.ts
          - test/integration/services/members/send-gift-reminders.test.js
          - test/integration/services/gifts/gift-delivery-processing.test.ts
    unverified: []
    blockers:
      - >-
        Add real-root export, repeated-init/changed-options, pre-init and partial-failure contracts.
        Failure after mail/scheduler construction can leave retained resources; failure after
        service publication suppresses retry and can leave missing subscriptions. Existing boot
        dependency fakes prove Stripe ordering, not construction rollback or readiness guards.
      - >-
        Define and test shutdown/restart ownership across domain handlers, unawaited boot recovery,
        external-transaction continuations, mail transport and signed scheduler registrations.
        Captured controller/job/event/analytics implementations do not follow replacement bindings.
        Per-invocation recovery limits do not bound overlapping recovery calls globally.
      - >-
        Cover production recovery's reschedule flag, error continuation, initial delivery/reminder
        registrations and key rotation using real root wiring. Only delivery callbacks are rebuilt
        by recoverPendingDeliveries; reminders rely on existing callbacks, key rotation or the daily
        job after restart. Deep scheduler tests and HTTP tests with dispatch stubbed do not prove
        the full callback-to-delivery path.
      - >-
        Characterize recurring-job concurrent scheduling, retry after rejection and same-process
        backend restart. Successful module flags are never reset; current cron regex bounds hours
        but not seconds/minutes. The production error wrapper can leave a signed time marked armed
        after a reported adapter failure; daily recovery is the backstop, not immediate retry.
      - >-
        Protect transaction/side-effect boundaries with real persistence fault tests: checkout
        delivery creation, Stripe session binding, member/gift/automation redemption, refund races
        and import reassignment. Unit transaction tokens/fulfilled executionPromise and invocation
        order do not prove commit/rollback; sequential webhook duplicate-row coverage does not prove
        concurrent completion or notification uniqueness.
      - >-
        Preserve and decide failure semantics explicitly: reminders may be missed after their
        marker commits; buyer notices have no durable retry; recipient mail can repeat after a
        crash or a send outlasting the one-hour stale threshold. Claims have no heartbeat/fencing
        token, and acceptance can precede failed persistence. Existing claim races and mocked mail
        are not evidence of crash-safe or exactly-once transport behavior.
      - >-
        Cover failed template/partial loads retained in renderer caches, real-root mail/config
        capture and delivery/analytics failure interaction. Existing policy/transport fakes and
        test config changes do not establish replacement behavior or production hot reload.
  identity-tokens:
    kind: directory
    disposition: migrate
    futureHome: core/server/auth/identity-tokens
    status: audited
    auditedAt: 5dae46983fe5423a900214dfc1906f5d14e637d2
    migration:
      complexity: straightforward
      modelImpact: fits
      reason: >-
        The guarded wrapper retains one token service with a captured issuer and live
        signing-key provider, and owns no resources. Replace instance access with the ready
        service, retain signing-keys-before-identity boot order, and update the identities
        endpoint and ActivityPub consumer. The merged root tests now characterize retention,
        live keys, pre-init access and retry after failed composition.
      reviewedAt: 61ac1e5058fa4657def92d7b362e197314b1a88d
      sources:
        - core/server/services/identity-tokens/identity-token-service-wrapper.js
        - core/server/services/identity-tokens/identity-token-service.ts
        - core/server/api/endpoints/identities.js
        - core/server/services/activitypub/activity-pub-service-wrapper.js
        - test/unit/server/services/identity-tokens/index.test.ts
      dependencies:
        complexity: straightforward
        reason: >-
          The wrapper already supplies the signing-key provider and captured issuer;
          jsonwebtoken.sign is the only remaining runtime library import in the implementation. The
          signing provider is read per token, so root wiring must preserve key rotation rather than
          capture a key. ActivityPub and the identities endpoint are the two production instance
          consumers to adapt.
        preparation:
          - >-
            Inject the token-signing function from the root, retaining direct imports of allowlisted
            libraries and the live signing-key provider.
          - >-
            Preserve signing-keys readiness before Identity Tokens and adapt ActivityPub and the
            identities endpoint together with the root export change.
        reviewedAt: 44ad405bff4a8700af2ed51f833ebc08d25dddff
        sources:
          - core/server/services/identity-tokens/identity-token-service-wrapper.js
          - core/server/services/identity-tokens/identity-token-service.ts
          - core/server/services/activitypub/activity-pub-service-wrapper.js
          - core/server/api/endpoints/identities.js
          - core/boot.js
    entryPoints:
      - identity-tokens/index.js
    notes: >-
      Suggested future home after this migration: server/auth/identity-tokens; Admin issuer, staff email/role claims and
      short token expiry are authentication policy. ActivityPub consumes that identity capability
      rather than owning it. Preserve the configured service at its destination; no source has
      moved. Staff identity-token issuance is boot-owned; preserve the captured issuer and
      identity provider while keeping signing-key selection live.
    contracts:
      exports:
        summary: >-
          The CommonJS root re-exports IdentityTokenServiceWrapper with static async init and
          writable instance, initially undefined. The implementation file separately exports
          the IdentityTokenService constructor and getTokenForUser instance method; there is
          no validation API or shutdown hook.
        sources:
          - core/server/services/identity-tokens/index.js
          - core/server/services/identity-tokens/identity-token-service-wrapper.js
          - core/server/services/identity-tokens/identity-token-service.ts
        tests:
          - test/unit/server/services/identity-tokens/index.test.ts
      construction:
        summary: >-
          The wrapper is the only production construction site found. First init captures the
          absolute admin URL and signingKeys.service.forPurpose(staff), then publishes
          IdentityTokenService synchronously within the async method. A truthy instance makes
          later calls return without reconstructing dependencies.
        sources:
          - core/server/services/identity-tokens/identity-token-service-wrapper.js
          - core/server/services/signing-keys/signing-key-service.ts
        tests:
          - test/unit/server/services/identity-tokens/index.test.ts
      consumers:
        summary: >-
          Boot calls init. The authenticated identities endpoint reads the current instance
          for each request, passing the user's email and loaded role; failed role loading logs
          a warning and passes null. ActivityPub instead retains the instance supplied during
          its initialization and uses it to sign an Owner token for remote configuration.
          Existing API tests verify Owner/Administrator JWTs through HTTP JWKS and reject an
          Editor; they do not cover wrapper replacement or ActivityPub's real token connection.
          Root tests cover successful role loading through the identities controller; the
          failed-role fallback and ActivityPub's dependency hand-off are source evidence.
        sources:
          - core/boot.js
          - core/server/api/endpoints/identities.js
          - core/server/web/api/endpoints/admin/routes.js
          - core/server/services/activitypub/activity-pub-service-wrapper.js
          - core/server/services/activitypub/activity-pub-service.ts
        tests:
          - test/legacy/api/admin/identities.test.js
          - test/unit/server/services/identity-tokens/index.test.ts
      startup:
        summary: >-
          initCore awaits signing-key initialization before initServices awaits identity init
          with other prerequisites. ActivityPub background init checks instance and logs then
          returns if it is absent; the identities endpoint dereferences it without that guard.
          Importing this root alone does not initialize signing keys or issue a token.
          Root tests cover import behavior, endpoint signing after explicit initialization,
          and retry after missing signing keys or provider-selection failure. Production boot
          ordering, ActivityPub's background startup guard and the endpoint's unguarded
          pre-init access are source evidence.
        sources:
          - core/boot.js
          - core/server/services/identity-tokens/identity-token-service-wrapper.js
          - core/server/api/endpoints/identities.js
          - core/server/services/activitypub/activity-pub-service-wrapper.js
        tests:
          - test/unit/server/services/identity-tokens/index.test.ts
      lifetime:
        summary: >-
          Normal repeated initialization retains one process-local instance, issuer and
          purpose provider, with no reset path. The provider closes over its SigningKeyService
          and reads that service's settings cache on use. Each token call allocates its own
          claims object; no user or role is stored on the shared service. Root tests verify
          retained instance, issuer and provider identity, with live signing-key lookups.
          The absence of a reset path, the provider's settings-cache access and isolation
          between calls for different users and roles are source evidence.
        sources:
          - core/server/services/identity-tokens/identity-token-service-wrapper.js
          - core/server/services/identity-tokens/identity-token-service.ts
          - core/server/services/signing-keys/signing-key-service.ts
        tests:
          - test/unit/server/services/identity-tokens/index.test.ts
      configuration:
        summary: >-
          The issuer URL is captured at construction; the retained provider supplies a key
          per call. Tokens use email as sub, include role only for string arguments, and use
          RS256, a five-minute expiry and the provider's kid. The unit test verifies a token's
          subject, role, issuer and kid. Root tests additionally verify a five-minute expiry,
          omitted role, retained issuer and use of the provider's changed signing key through
          a retained instance. This does not exercise a full signing-key rotation workflow.
        sources:
          - core/server/services/identity-tokens/identity-token-service-wrapper.js
          - core/server/services/identity-tokens/identity-token-service.ts
          - core/server/services/signing-keys/signing-key-service.ts
        tests:
          - test/unit/server/services/identity-tokens/identity-token-service.test.ts
          - test/unit/server/services/identity-tokens/index.test.ts
      resources:
        summary: >-
          This service stores only issuer and provider references; it acquires no timer,
          listener, network client or database connection. Signing uses the shared key
          service and its public-key metadata cache. The wrapper has no cleanup registration
          or tracking of in-flight token calls; key storage and rotation belong to dependencies.
        sources:
          - core/server/services/identity-tokens/identity-token-service-wrapper.js
          - core/server/services/identity-tokens/identity-token-service.ts
          - core/server/services/signing-keys/signing-key-service.ts
          - core/server/lib/public-jwk.ts
        tests: []
      failures:
        summary: >-
          Dependency lookup or construction failure rejects init before instance assignment,
          leaving a later call able to try again. Missing/invalid signing keys and JWT signing
          failures reject token issuance without a local retry or fallback. The endpoint only
          catches role lookup failures; ActivityPub catches token/request failures in its own
          remote-configuration methods. Root tests assert missing-prerequisite and provider
          selection retries, plus token lookup failure and later success on the same instance.
        sources:
          - core/server/services/identity-tokens/identity-token-service-wrapper.js
          - core/server/services/identity-tokens/identity-token-service.ts
          - core/server/api/endpoints/identities.js
          - core/server/services/activitypub/activity-pub-service.ts
        tests:
          - test/unit/server/services/identity-tokens/index.test.ts
    unverified: []
    blockers:
      - >-
        Preserve real boot ordering and retained consumers through a complete signing-key
        rotation or same-process restart. Root tests now cover missing-prerequisite retry,
        repeated init, captured issuer and a changed key returned by the retained provider;
        they do not exercise the full rotation workflow or define restart ownership.
      - >-
        Characterize concurrent token calls and ActivityPub's real token connection. The root
        suite verifies the real identities controller response, optional role, exact expiry
        and key lookup failure recovery. Actual JWT signing failure recovery remains untested;
        ActivityPub's implementation tests use a fake token service.

  indexnow-ping:
    kind: directory
    disposition: migrate
    migrated: true
    status: audited
    auditedAt: 5dae46983fe5423a900214dfc1906f5d14e637d2
    migration:
      complexity: straightforward
      modelImpact: fits
      reason: >-
        Already migrated: the factory creates one instance, registers its two listeners, and
        removes partial registrations before allowing retry. Boot awaits publication. This
        is the existing example for a bounded subscription-owning service; detached requests
        and normal shutdown remain separate concerns.
      reviewedAt: f276513ba32b7114fd5cc4f7b02d3f82c0c31e3e
      sources:
        - core/server/services/indexnow-ping/index.ts
        - core/server/services/indexnow-ping/indexnow-ping-service.ts
        - core/boot.js
        - test/unit/server/services/indexnow-ping/index.test.ts
    entryPoints:
      - indexnow-ping/index.ts
    notes: >-
      Preserve post-event gating, live settings and request error reporting. Shared initialization
      covers publication and retry; restart, request disposal and production retries remain open.
    contracts:
      exports:
        summary: >-
          The TypeScript default export is a defineService root exposing init(): Promise<void>
          and a read-only service getter. CommonJS boot uses .default; init resolves without an
          instance. Reading service before publication throws IncorrectUsageError. The
          implementation exports IndexNowPingService for direct construction and exposes its
          dependencies, bound listener and operation methods. Root tests assert guarded access.
        sources:
          - core/server/services/indexnow-ping/index.ts
          - core/server/services/indexnow-ping/indexnow-ping-service.ts
          - core/kernel/define-service.ts
          - core/boot.js
        tests:
          - test/unit/server/services/indexnow-ping/index.test.ts
      construction:
        summary: >-
          Import creates only the root. First init loads shared settings/config, URL services,
          request, logging and model events, constructs the implementation and subscribes before
          publication. Concurrent calls share initialization; successful later calls reuse it.
          The root is the only runtime construction path found. Root tests use the shared event
          bus and request helper with stubbed settings/config/URL lookup; helper tests cover
          concurrent initialization. Class tests inject fakes except for the request helper.
        sources:
          - core/server/services/indexnow-ping/index.ts
          - core/server/services/indexnow-ping/indexnow-ping-service.ts
          - core/kernel/define-service.ts
        tests:
          - test/unit/server/services/indexnow-ping/index.test.ts
          - test/unit/kernel/define-service.test.ts
      consumers:
        summary: >-
          Boot is the only runtime root importer. Post model events invoke the listener for
          post.published and post.published.edited, after commit when a transaction is supplied.
          Import events and events without changed SEO fields return early; others pass post
          JSON plus authors and tags to a detached ping. No runtime deep constructor or direct
          ping caller was found. The frontend key-verification middleware independently reads
          the same setting. Root tests cover event gating and relation forwarding; an HTTP test
          publishes through an author/tag-filtered collection. Transaction rollback and HTTP
          import suppression remain source-only here.
        sources:
          - core/boot.js
          - core/server/models/post.js
          - core/server/models/base/plugins/events.js
          - core/server/services/indexnow-ping/indexnow-ping-service.ts
          - core/server/services/indexnow-ping/constants.ts
          - core/server/services/content-import/import/importer.ts
          - core/frontend/web/middleware/serve-indexnow-key.js
        tests:
          - test/unit/server/services/indexnow-ping/indexnow-ping-service.test.js
          - test/unit/server/services/indexnow-ping/index.test.ts
          - test/e2e-server/indexnow-publishing.isolated.test.ts
      startup:
        summary: >-
          Boot initializes settings and dynamic routing before awaiting init in its required
          service Promise.all, before readiness, including backend-only boot. Subscription runs
          synchronously during init without checking keys or sending a request; publication and
          init completion are asynchronous. Disabled/private/development configurations still
          subscribe. The settings model generates the key as a default, and frontend middleware
          serves its matching path. Initialization failure rejects required startup. Root tests
          cover disabled first init; the HTTP test boots with the production key definition and
          serves the generated key. Backend-only ordering remains source evidence.
        sources:
          - core/boot.js
          - core/server/services/indexnow-ping/index.ts
          - core/server/services/indexnow-ping/indexnow-ping-service.ts
          - core/kernel/define-service.ts
          - core/server/models/settings.js
          - core/server/data/schema/default-settings/default-settings.json
          - core/frontend/web/site.js
          - core/frontend/web/middleware/serve-indexnow-key.js
        tests:
          - test/unit/server/services/indexnow-ping/index.test.ts
          - test/e2e-server/indexnow-publishing.isolated.test.ts
      lifetime:
        summary: >-
          The root retains one implementation and its bound listener. Direct subscribeEvents
          removes that callback before registering it for each event; unsubscribeEvents removes
          only that instance's callbacks. Successful repeated init does not restore externally
          removed listeners. Neither root nor implementation exposes reset or shutdown.
          EventEmitter delivery is synchronous; ping completion is detached, with no
          serialization, deduplication of pending work or retained completion handle. Root tests
          assert retained identity, one callback per event and event delivery returning while a
          ping is pending; the HTTP test completes publication before its request settles.
        sources:
          - core/server/services/indexnow-ping/index.ts
          - core/server/services/indexnow-ping/indexnow-ping-service.ts
          - core/kernel/define-service.ts
          - core/server/lib/common/events.js
          - core/boot.js
          - core/server/ghost-server.ts
        tests:
          - test/unit/server/services/indexnow-ping/index.test.ts
          - test/e2e-server/indexnow-publishing.isolated.test.ts
      configuration:
        summary: >-
          Each ping reads live environment, site privacy, privacy.useIndexNow, key and URL
          providers. Development, pages, private sites, disabled privacy and listed default
          slugs skip delivery. SEO fields and the IndexNow endpoint are module constants.
          Requests carry url, key and keyLocation with a five-second request timeout. Key
          lookup accepts any truthy value, while the verification middleware requires an exact
          matching 32-character lowercase hex path. Root tests cover live flags/key/URL changes
          and complete query encoding through the request helper. The HTTP test verifies a
          generated key, response headers and keyLocation on a subdirectory site; malformed
          keys and verification-route changes remain untested here.
        sources:
          - core/server/services/indexnow-ping/indexnow-ping-service.ts
          - core/server/services/indexnow-ping/constants.ts
          - core/frontend/web/middleware/serve-indexnow-key.js
          - core/server/models/settings.js
        tests:
          - test/unit/server/services/indexnow-ping/indexnow-ping-service.test.js
          - test/unit/server/services/indexnow-ping/index.test.ts
          - test/e2e-server/indexnow-publishing.isolated.test.ts
      resources:
        summary: >-
          The implementation owns two model-event registrations and starts untracked HTTP
          requests. The factory removes partial subscriptions after startup failure; root tests
          assert cleanup preserves unrelated callbacks and retry installs one of each listener.
          Boot registers no cleanup; no request cancellation or drain is exposed. The shared
          request dependency owns DNS lookup/cache state and unref'ed cache-maintenance timers;
          HTTP timeout and retry work belongs to that helper and Got. This root creates no
          database connection, dedicated HTTP agent, scheduler or durable queue. Its borrowed
          settings, URL services and event bus outlive individual deliveries.
        sources:
          - core/server/services/indexnow-ping/index.ts
          - core/server/services/indexnow-ping/indexnow-ping-service.ts
          - core/server/lib/common/events.js
          - core/boot.js
          - core/server/ghost-server.ts
          - package.json
        tests:
          - test/unit/server/services/indexnow-ping/index.test.ts
      failures:
        summary: >-
          Subscription failure rejects init without publication after removing partial listeners;
          root tests assert the original error and a successful retry. Missing/unresolved URLs
          and absent keys warn and return; 200/202 log success, 429 logs rate_limited, 403/422
          key_validation_failed and other errors ping_failed. Ordinary URL-resolution/request
          errors are logged and swallowed. Root tests assert synchronous model errors escape
          and pre-try config rejections are swallowed by the listener. The HTTP test retains a
          published post after a deferred transport failure. Production GETs inherit Got retries
          despite the no-retry comment; the request helper disables retries under test
          environments. These tests do not pin production attempts, timeout duration or logger
          failure behavior.
        sources:
          - core/server/services/indexnow-ping/index.ts
          - core/server/services/indexnow-ping/indexnow-ping-service.ts
          - core/kernel/define-service.ts
          - core/server/models/base/plugins/events.js
          - package.json
          - vitest.config.ts
        tests:
          - test/unit/server/services/indexnow-ping/indexnow-ping-service.test.js
          - test/unit/server/services/indexnow-ping/index.test.ts
          - test/e2e-server/indexnow-publishing.isolated.test.ts
    unverified: []
    blockers:
      - >-
        Decide listener disposal, restart after external listener removal and pending-request
        drain ownership. Partial-startup cleanup and repeated init are covered; normal shutdown
        has no cleanup hook. Backend-only startup and failed required boot still need coverage.
      - >-
        Cover transaction rollback and import suppression through the real publishing path,
        plus logging failures. The HTTP test covers successful filtered publication and a
        later transport failure; event-gating tests use fake post models.
      - >-
        Cover malformed/missing verification keys and key changes through the HTTP route.
        The route and ping accept different key shapes; the generated-key success case is
        covered, but existing getApiKey tests also deliberately accept a non-hex key.
      - >-
        Resolve the no-retry comment against actual transport behavior: production Got GETs
        permit two retries for eligible statuses/errors, while test mode disables retries.
        Characterize production backoff/timeouts and stop with pending requests; the current
        HTTP classification tests do not establish these lifecycle contracts.
  integrations:
    kind: directory
    disposition: migrate
    status: audited
    migrated: true
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: straightforward
      modelImpact: fits
      reason: >-
        The boot-owned defineService root constructs one instance with the shared Integration
        and ApiKey models. initCore awaits initialization before the endpoint reads that
        ready instance during edit requests. Existing editing and key-rotation behavior stays
        in the unchanged implementation; no extras or lifecycle extension is needed.
      reviewedAt: 8de2cd47ce1614f877695695a48b41e69f26e650
      sources:
        - core/server/services/integrations/index.ts
        - core/server/services/integrations/integrations-service.js
        - core/server/api/endpoints/integrations.js
        - core/boot.js
        - test/e2e-api/admin/integrations.test.js
      dependencies:
        complexity: straightforward
        reason: >-
          The implementation already receives both Integration and ApiKey models and directly
          imports only allowlisted errors and tpl. The Admin endpoint currently constructs its
          instance, so the dependency work is moving that existing composition to the boot-owned
          root and updating the endpoint's edit delegate.
        preparation:
          - >-
            Construct the existing model-backed implementation in index.ts after models are ready,
            and have the endpoint retrieve that ready instance instead of constructing one during
            module loading.
        reviewedAt: 44ad405bff4a8700af2ed51f833ebc08d25dddff
        sources:
          - core/server/services/integrations/integrations-service.js
          - core/server/api/endpoints/integrations.js
    entryPoints:
      - integrations/index.ts
    notes: >-
      Migration inspected 8de2cd47ce1614f877695695a48b41e69f26e650 after the characterization
      prerequisite merged. Boot now owns the instance; the endpoint retains the root, reads
      its service inside the edit query, and the implementation stays unchanged.
      Characterization originally prepared against
      e7ab0fe5a8406036cfb0d790e8db7afdb58bfe36 protects model receivers, option handling and
      awaited edit/rotation results. auditedAt and the dependency assessment retain historical
      provenance. Initialization owns no resources; same-process reboot, transaction,
      association and action-write guarantees remain outside this bounded change.
    contracts:
      exports:
        summary: >-
          The TypeScript root default-exports defineService with init and guarded service;
          it exposes no extras. service is the retained IntegrationsService with async
          edit(data, options) and borrowed IntegrationModel/ApiKeyModel fields. edit still
          requires its receiver and an options object. The private CommonJS implementation
          retains its receiver-independent factory for construction and implementation tests;
          normal runtime callers use the ready root. No reset or shutdown is exposed.
        sources:
          - core/server/services/integrations/index.ts
          - core/server/services/integrations/integrations-service.js
        tests:
          - test/unit/server/services/integrations/integrations-service.test.ts
      construction:
        summary: >-
          The boot-owned root is the only production factory caller. Its init factory supplies
          the shared Integration and ApiKey models and returns the existing implementation.
          Construction only stores model references; it makes no query or subscription. The
          endpoint retains the root and reads the ready instance inside its edit query, not at
          module evaluation. Root units import the real endpoint before init and assert the
          factory has not run, then initialize the root before exercising edits. They prove idle
          composition and retained receiver identity with stubbed models, not persisted behavior.
        sources:
          - core/boot.js
          - core/server/services/integrations/index.ts
          - core/server/services/integrations/integrations-service.js
          - core/server/api/endpoints/integrations.js
        tests:
          - test/unit/server/services/integrations/integrations-service.test.ts
          - test/unit/server/services/integrations/index.test.ts
      consumers:
        summary: >-
          The endpoint delegates ordinary integration edits and API-key refresh requests to
          this retained instance; browse/read/add/destroy use models directly. Both edit routes
          run through Admin authentication, API permissions and input/output serialization.
          HTTP tests exercise ordinary field/webhook edits, successful admin-key rotation and
          missing-integration edits. The rotation case checks the new secret and a refreshed
          action, but not old-token rejection, content keys or mismatched integration/key IDs.
          Preparation units exercise the real endpoint and retained instance across ordinary
          edits and rotation, preserving receivers, current results and pending/error forwarding.
        sources:
          - core/server/api/endpoints/integrations.js
          - core/server/web/api/endpoints/admin/routes.js
          - core/server/api/endpoints/utils/serializers/input/integrations.js
          - core/server/api/endpoints/utils/serializers/output/mappers/integrations.js
          - core/server/models/integration.js
        tests:
          - test/e2e-api/admin/integrations.test.js
          - test/unit/server/services/integrations/index.test.ts
      startup:
        summary: >-
          initCore awaits Integrations initialization after Invites and before Stripe Checkout
          Config, frontend initialization and API request serving. The root defers model
          acquisition and construction until init, and its guarded getter rejects pre-init
          access according to the kernel contract. Admin router construction still uses the lazy endpoint getter, and
          loading the endpoint reads no service, so router construction does not depend on
          initialization; each edit request reads the published instance. Existing HTTP cases
          exercise real boot and request composition; adapted preparation fixtures explicitly
          await root initialization. initCore orchestration units hold the root's init promise
          pending and reject it, proving awaited readiness and unchanged error propagation
          before Stripe Checkout Config starts. These units stub the root; real factory failure
          and same-process restart behavior remain untested.
        sources:
          - core/server/services/integrations/index.ts
          - core/boot.js
          - core/server/api/endpoints/index.js
          - core/shared/express.js
          - core/server/web/parent/backend.js
          - core/server/web/api/app.js
          - core/server/web/api/endpoints/admin/app.js
          - core/server/web/api/endpoints/admin/routes.js
          - core/server/api/endpoints/integrations.js
          - core/server/services/integrations/integrations-service.js
        tests:
          - test/unit/boot/init-core.test.ts
          - test/e2e-api/admin/integrations.test.js
          - test/unit/server/services/integrations/index.test.ts
      lifetime:
        summary: >-
          The root retains one instance after successful init; repeated init does not replace
          it under the kernel contract. The endpoint reads that same instance through the root on
          each edit request, sharing its model references. Each edit queries current rows and
          keeps no per-integration cache. Ordinary edit mutates the caller's options with require
          true; rotation instead clones options for refresh and uses fresh relation options for
          reread. Replacing the root's module export would not update the endpoint's private root
          reference. Retained-endpoint
          units cover repeated edits; no same-process reboot, serialization, deduplication or
          concurrency guard around edits/rotations is established.
        sources:
          - core/boot.js
          - core/server/services/integrations/index.ts
          - core/server/services/integrations/integrations-service.js
          - core/server/api/endpoints/integrations.js
        tests:
          - test/unit/server/services/integrations/integrations-service.test.ts
          - test/unit/server/services/integrations/index.test.ts
      configuration:
        summary: >-
          Root initialization and the implementation read no application configuration; the
          ready instance retains the model dependencies supplied at boot.
          Truthy options.keyid selects rotation and ignores edit data: it reads the key by
          keyid alone, refreshes that key and then reads the integration named by options.id
          with api_keys/webhooks. The service does not compare the key's integration_id with
          options.id. Ordinary edits forward data and options to the model. HTTP serialization
          limits editable fields and formats admin secrets as id:secret; these are separate
          boundaries, not validation implemented by the service factory. The ApiKey unit test
          stubs model edit and pins generated secret length and forwarded data/options; it does
          not exercise service composition, association checks or persisted rotation.
          Service units now pin ordinary option mutation, rotation option cloning, caller
          transaction propagation to refresh only, and admin/content key delegation through
          stubbed models; they do not prove stored key lengths or transaction atomicity.
        sources:
          - core/server/services/integrations/integrations-service.js
          - core/server/api/endpoints/integrations.js
          - core/server/api/endpoints/utils/serializers/input/integrations.js
          - core/server/api/endpoints/utils/serializers/output/mappers/integrations.js
          - core/server/models/api-key.js
        tests:
          - test/e2e-api/admin/integrations.test.js
          - test/unit/server/models/api-key.test.ts
          - test/unit/server/services/integrations/integrations-service.test.ts
      resources:
        summary: >-
          The instance owns only borrowed model references; edits use the shared database and
          model event/action machinery. It creates no timer, network client or listener and
          has no pending-query drain. Rotation has no service-level transaction joining key
          lookup, refresh and integration reread. Lookup/reread omit caller transaction options,
          although refresh receives them. ApiKey.onUpdated starts refreshed-action recording
          without returning its promise; success of rotation does not itself await that write.
          The action plugin catches/logs ordinary Action.add rejection. With a caller transaction
          it waits for a truthy committed event, then inserts the action outside that transaction.
          HTTP tests observe a later action row, not write timing or failure handling.
          Service units hold refresh and reread promises separately to prove operation ordering
          and settlement; removing the refresh await makes the new pending-operation case fail.
          Stubbed transactions do not establish database commit or rollback behavior.
        sources:
          - core/server/services/integrations/integrations-service.js
          - core/server/models/api-key.js
          - core/server/models/integration.js
          - core/server/models/base/plugins/crud.js
          - core/server/models/base/plugins/actions.js
          - core/server/models/base/plugins/events.js
        tests:
          - test/e2e-api/admin/integrations.test.js
          - test/unit/server/services/integrations/integrations-service.test.ts
      failures:
        summary: >-
          Key lookup rejection escapes unchanged; an absent key yields NotFoundError before
          the wrapping catch. Refresh, key serialization and subsequent integration-read
          failures become InternalServerError, potentially after the secret changed. A null
          integration reread is returned without a service error. Ordinary edits translate
          errors whose message is NotFound or EmptyResponse to Integration NotFoundError and
          rethrow others. Ordinary action-write rejection is logged without rejecting or rolling
          back rotation. HTTP coverage pins deleted-integration 404/context. Preparation units
          pin missing/rejected key lookup, serialization/refresh/reread failure wrapping with
          the generic 500, null reread, ordinary NotFound/EmptyResponse translation and unchanged
          unrelated error identity. These stubbed model checks do not establish partial writes
          or action-record failure behavior.
        sources:
          - core/server/services/integrations/integrations-service.js
          - core/server/models/api-key.js
          - core/server/models/base/plugins/crud.js
          - core/server/models/base/plugins/actions.js
          - core/server/api/endpoints/integrations.js
        tests:
          - test/e2e-api/admin/integrations.test.js
          - test/unit/server/services/integrations/integrations-service.test.ts
          - test/unit/server/services/integrations/index.test.ts
    unverified: []
    blockers:
      - >-
        Add persisted key-rotation failure contracts, including action-record failure. The
        new service units pin failure translation through stubbed models, not stored-secret
        outcomes; a successful HTTP rotation and later action lookup do not prove atomicity
        or that the action is awaited. Preserve current transaction propagation in a bounded
        initializer migration; changing its ownership requires separate preparation.
      - >-
        Characterize integration/key association and concurrent rotations through authorized
        HTTP calls, including content keys and old-token rejection. Source shows independent
        key/integration lookups; do not assume the facade or existing successful path tests
        establish association validation or protection against lost/repeated rotation results.
      - >-
        Preparation now pins direct model-failure translation and pending edit/rotation
        settlement. Resource drain, cleanup, same-process reboot and transaction rollback remain
        unproven; do not introduce those guarantees as part of initializer adoption.
  internal-keys:
    kind: directory
    disposition: migrate
    futureHome: core/server/auth/internal-keys
    status: audited
    migrated: true
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: straightforward
      modelImpact: fits
      reason: >-
        Boot constructs one key map through defineService in initCore, before frontend and
        scheduling consumers. Authentication and frontend access the same ready map. Lazy
        database lookup and clear/delete invalidation remain map operations, including caching rejected
        promises. Merged tests characterize shared pending lookups, failure retention,
        invalidation and the retained frontend consumer.
      reviewedAt: 326a1a3e711690149928fcb69e6fbc5516213ba9
      sources:
        - core/server/services/internal-keys/index.ts
        - core/server/services/post-scheduling/index.ts
        - core/server/auth/reset-authentication.ts
        - core/frontend/services/proxy.js
        - test/unit/server/services/internal-keys/index.test.ts
      dependencies:
        complexity: straightforward
        reason: >-
          Both AutoFillingMap and the model registry are already imported by the root, and the map's
          compute callback defers database access until get(). Keep one map for scheduling,
          authentication and the frontend, preserving clear/delete invalidation without preloading
          keys.
        preparation: []
        reviewedAt: 44ad405bff4a8700af2ed51f833ebc08d25dddff
        sources:
          - core/server/services/internal-keys/index.ts
          - core/server/lib/auto-filling-map.ts
          - core/server/services/post-scheduling/index.ts
          - core/server/auth/reset-authentication.ts
          - core/frontend/services/proxy.js
    entryPoints:
      - internal-keys/index.ts
    notes: >-
      Suggested future home after this migration: server/auth/internal-keys; the scheduler/frontend credential map is
      authentication support, and authentication reset already owns its invalidation. The
      initializer migration remains valid; moving ownership must preserve the same ready map. No
      source has moved. Initializer migration reviewed against main
      326a1a3e711690149928fcb69e6fbc5516213ba9; historical audit provenance is retained. The
      shared mutable map, lazy database reads and explicit invalidation are unchanged. Promise
      retention or rotation changes need separate tests and a decision.
    contracts:
      exports:
        summary: >-
          The TypeScript default export is a defineService root with init and a guarded service
          getter; CommonJS consumers use .default. The ready instance is one AutoFillingMap
          from integration slugs to API-key promises. Other exports are types only; there are
          no extras. The full inherited Map surface remains available, including set, delete
          and clear; get requires its receiver. Normal shutdown ownership is unchanged.
        sources:
          - core/server/services/internal-keys/index.ts
          - core/server/lib/auto-filling-map.ts
          - core/frontend/services/proxy.js
        tests: []
      construction:
        summary: >-
          The root factory acquires the model registry and constructs an empty map when boot
          awaits init, without querying keys. A cache miss calls Integration.getApiKeyBySlug
          with the fixed slug-to-type mapping and stores its returned promise immediately. The model joins integrations and
          api_keys and returns a flat id/secret object. No alternate runtime map construction
          for this capability was found. Root tests exercise the actual exported map with
          controlled model lookups, including exact slug/type selection and shared promises.
        sources:
          - core/server/services/internal-keys/index.ts
          - core/server/lib/auto-filling-map.ts
          - core/server/models/integration.js
        tests:
          - test/unit/server/lib/auto-filling-map.test.ts
          - test/unit/server/models/integration.test.js
          - test/unit/server/services/internal-keys/index.test.ts
      consumers:
        summary: >-
          Frontend proxy getFrontendKey exposes the content secret to content_api_key,
          comments and ghost_head helpers. Post scheduling retains the map; boot supplies it
          to gifts' two SignedFlushSchedulers and automations' scheduled poll callback. Auth
          reset reads the old scheduler key, rotates secrets transactionally and clears the
          same map before deleting sessions and rescheduling with the previous key.
        sources:
          - core/frontend/services/proxy.js
          - core/frontend/helpers/content_api_key.js
          - core/frontend/helpers/comments.js
          - core/frontend/helpers/ghost_head.js
          - core/boot.js
          - core/server/services/post-scheduling/index.ts
          - core/server/services/post-scheduling/post-scheduling.ts
          - core/server/services/gifts/index.ts
          - core/server/adapters/scheduling/signed-flush-scheduler.ts
          - core/server/services/automations/service.ts
          - core/server/auth/reset-authentication.ts
        tests:
          - test/unit/frontend/helpers/content-api-key.test.ts
          - test/unit/server/auth/reset-authentication.test.ts
          - test/unit/server/adapters/scheduling/signed-flush-scheduler.test.ts
          - test/unit/server/services/internal-keys/index.test.ts
          - test/e2e-server/internal-keys.isolated.test.ts
      startup:
        summary: >-
          Boot awaits init in initCore before frontend, API and other service loading. Post
          scheduling retains the ready map during its own construction; boot supplies the same
          map to gifts and automations. Frontend and authentication modules can import the root
          early but resolve service only when invoked. Reading service before init throws.
          The first get still needs initialized models/database and seeded integration keys.
          A cold-boot HTTP test uses the retained frontend/map with real seeded keys and
          authentication rotation. Empty-map readiness does not preload keys; same-process
          reboot and signed-job rebuilding are separate obligations.
        sources:
          - core/boot.js
          - core/server/services/internal-keys/index.ts
          - core/server/services/post-scheduling/post-scheduling.ts
          - core/server/services/gifts/index.ts
          - core/server/adapters/scheduling/signed-flush-scheduler.ts
          - core/server/services/automations/service.ts
          - core/frontend/services/proxy.js
        tests:
          - test/e2e-server/internal-keys.isolated.test.ts
      lifetime:
        summary: >-
          All callers share one process-local map. Same-slug concurrent reads share its promise;
          fulfilled and rejected promises remain until explicit delete/clear. Invalidation
          makes later get calls reload but does not cancel an outstanding query or invalidate
          an already retained key object. A promise settling after clear does not repopulate
          the map. Server shutdown does not clear the map. Helper tests
          prove memoized success and invalidation. Root tests additionally cover pending and
          rejected promise retention, late completion after clear and a retained frontend
          consumer reading the replacement key. A root unit calls real auth-reset orchestration
          with its default map and controlled domain/scheduler collaborators: old keys remain
          through a held commit, then sessions are deleted before rescheduling with the old
          key while both consumers read replacements. A cold-boot HTTP test also rotates the
          persisted keys through authentication/reset, verifies both retained consumers read
          the new secrets, checks the real scheduling adapter receives the previous key, and
          verifies both persisted staff sessions are removed and rejects their subsequent
          requests. It does not assert rebuilding signed jobs. Other auth
          unit tests use an injected map.
        sources:
          - core/server/services/internal-keys/index.ts
          - core/server/lib/auto-filling-map.ts
          - core/server/auth/reset-authentication.ts
          - core/boot.js
        tests:
          - test/unit/server/lib/auto-filling-map.test.ts
          - test/unit/server/auth/reset-authentication.test.ts
          - test/unit/server/services/internal-keys/index.test.ts
          - test/e2e-server/internal-keys.isolated.test.ts
      configuration:
        summary: >-
          ghost-scheduler selects an admin key and ghost-internal-frontend selects a content
          key. There are no config flags or environment gates in the root. The first miss reads
          current database values; external changes do not automatically refresh a cached key.
          Auth reset is the production invalidation caller found, after its transaction commits.
        sources:
          - core/server/services/internal-keys/index.ts
          - core/server/models/integration.js
          - core/server/auth/reset-authentication.ts
        tests:
          - test/unit/server/models/integration.test.js
          - test/unit/server/auth/reset-authentication.test.ts
          - test/unit/server/services/internal-keys/index.test.ts
          - test/e2e-server/internal-keys.isolated.test.ts
      resources:
        summary: >-
          The map owns cached promises and their key objects and borrows the model/database
          owner for queries; it acquires no connection, timer, listener or scheduler itself.
          clear/delete release only map references and do not drain lookups. Scheduler resources
          and derived signed jobs belong to consumers, which must rebuild them after rotation.
        sources:
          - core/server/services/internal-keys/index.ts
          - core/server/lib/auto-filling-map.ts
          - core/server/models/integration.js
          - core/server/auth/reset-authentication.ts
          - core/server/adapters/scheduling/signed-flush-scheduler.ts
        tests: []
      failures:
        summary: >-
          Missing rows reject with NotFoundError; database failures propagate and remain cached
          as rejected promises. Frontend proxy logs lookup failures and returns null; scheduling
          event callbacks log them; PostScheduling and SignedFlushScheduler rescheduleAll
          reject. Future automation polls catch key failure after arming an in-memory timer.
          Auth reset cannot start rotation
          if the previous-key lookup fails; transaction failure leaves the cache intact, whereas
          session/reschedule failure happens after commit and invalidation. The initializer
          permits an explicit retry if construction fails, but it does not retry or evict failed
          key lookups. Lookup retention and errors are covered by existing root/model tests;
          generic initialization readiness and retry are covered by kernel tests.
        sources:
          - core/server/models/integration.js
          - core/server/lib/auto-filling-map.ts
          - core/frontend/services/proxy.js
          - core/server/services/post-scheduling/post-scheduling.ts
          - core/server/adapters/scheduling/signed-flush-scheduler.ts
          - core/server/services/automations/service.ts
          - core/server/auth/reset-authentication.ts
        tests:
          - test/unit/server/models/integration.test.js
          - test/unit/server/auth/reset-authentication.test.ts
          - test/unit/server/services/internal-keys/index.test.ts
          - test/unit/kernel/define-service.test.ts
    unverified: []
    blockers:
      - >-
        Same-process reboot remains untested. The initializer retains the same map after
        successful init and shutdown still leaves its contents intact. Actual-root units cover
        slug/type mapping, shared pending lookups, rejection retention until invalidation and
        the retained frontend consumer. Cold-boot HTTP covers real key availability and rotation.
      - >-
        Test signed job rebuilding and post-commit session/reschedule failures. Root units
        cover default-map auth orchestration with a held fake transaction, frontend invalidation
        and a pending lookup completing after clear. Cold-boot HTTP covers persisted key rotation,
        persisted deletion of both staff sessions and previous-key delegation to the actual
        scheduling adapter,
        without asserting scheduled-job rebuilding. Neither clear nor rotation revokes
        promises/key objects already held by callers or updates already signed jobs by itself.
      - >-
        Settle cache ownership and invalidation before introducing restart/replacement or
        external key refresh. This initializer migration preserves the existing policy: no
        automatic expiry, failure eviction or shutdown clear. TypeScript slug types do not
        enforce valid keys at runtime.
  invites:
    kind: directory
    disposition: migrate
    status: audited
    migrated: true
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: straightforward
      modelImpact: fits
      reason: >-
        Move the eager Invites instance into a boot factory and change the endpoint to use
        the ready instance. Its constructor only captures facades; invitation persistence,
        rendering and API-mail delegation happen per call. Acceptance now lives under this
        owner as an invocation-local action; preserve that separate function and its
        transaction behavior.
      reviewedAt: 61ac1e5058fa4657def92d7b362e197314b1a88d
      sources:
        - core/server/services/invites/index.ts
        - core/server/services/invites/invites.ts
        - core/server/services/invites/accept.ts
        - core/server/api/endpoints/invites.js
        - core/server/api/endpoints/authentication.js
      dependencies:
        complexity: straightforward
        reason: >-
          Sending already receives settings, URL and mail facades, plus API/model collaborators per
          operation. The remaining logging and security imports are small injection changes.
          Acceptance is a separately deep-imported function with global models/security, so it needs
          its own injected construction without moving transaction state or invitation data into a
          retained singleton.
        preparation:
          - >-
            Inject logger and token encoding/decoding functions from index.ts while leaving settings
            access live.
          - >-
            Give the acceptance action explicit model/security dependencies and update the
            authentication endpoint, preserving its invocation-local transaction and the existing
            API-mail delegation for sends.
        reviewedAt: 44ad405bff4a8700af2ed51f833ebc08d25dddff
        sources:
          - core/server/services/invites/index.ts
          - core/server/services/invites/invites.ts
          - core/server/services/invites/accept.ts
          - core/server/api/endpoints/invites.js
          - core/server/api/endpoints/authentication.js
    entryPoints:
      - invites/index.ts
    notes: >-
      Move eager composition to boot while preserving invitation persistence, rendering and
      mail behavior. API-mail delegation and its transport ownership need a separate decision.
      Acceptance is an invocation-local domain action kept under this owner; do not add a
      lifecycle or change the sending instance as part of its relocation. Root units now use
      the real composition with controlled models/rendering/API mail to assert payloads, live
      settings, sequential resend, held mail delivery and step failures; these are not real
      persistence rollback or provider-delivery tests. Acceptance coverage was expanded against
      main 4509d04ab76c837343d12faa6dc269895bc9bba5 without changing production; the historical
      audit provenance remains unchanged. These units control models and the transaction,
      so they do not prove persisted rollback, row-lock behavior or committed model events.
      Dependency preparation against main b96cf86e5068863ae624183f1cf24eb1b97aa947 now wires
      models and logging in the existing root and exposes acceptance on that retained instance.
      The action receives models explicitly; its transaction remains invocation-local. The exact
      @tryghost/security package is allowed as library primitives, so its codecs stay directly
      imported rather than injected as the older dependency assessment proposed.
      Initializer migration against main fc865173c57abf76bbcefd22691e0c0b58443fac now constructs
      that same Invites class during initCore, before API loading. The default root exposes init
      and service, with add and accept on the retained instance and no extras. The implementation,
      transaction ownership, live settings reads and API-mail delegation are unchanged. Existing
      tests receive only initializer/import/fixture adaptations; audit provenance is preserved.
    contracts:
      exports:
        summary: >-
          The TypeScript root default-exports defineService with init and the guarded service
          getter, with no extras or other runtime exports. CommonJS callers read default. The
          ready service is one Invites instance whose add method needs its receiver; it retains
          settingsCache, settingsHelpers, mailService, urlUtils, models and logging facades.
          The deep invites.ts module exports the named Invites constructor. There is no shutdown or reset;
          add resolves the edited Invite model. The instance also exposes accept, delegating to
          the named accept.ts action
          with its injected models. It resolves undefined on success; the API serializer
          supplies the success message. The action adds no application instance, init or
          shutdown. Its former lazy barrel is preserved at
          https://github.com/TryGhost/Ghost/blob/bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e/ghost/core/core/server/services/invitations/index.js.
          The authentication endpoint imports the default Invites root and calls service.accept;
          sending callers use service.add, preserving its receiver and operation arguments.
        sources:
          - core/kernel/define-service.ts
          - core/server/services/invites/index.ts
          - core/server/services/invites/invites.ts
          - core/server/services/invites/accept.ts
          - core/server/api/endpoints/utils/serializers/output/authentication.js
        tests:
          - test/unit/server/services/invites/index.test.ts
      construction:
        summary: >-
          The root is the only production constructor found. Import declares the initializer;
          boot's awaited init call constructs Invites with shared settings, helpers, mail library,
          URL utilities, models and logging. The constructor stores these dependencies without
          starting invitation work. Acquiring the mail library constructs a reusable content
          generator, not a GhostMailer. Each add receives API facade,
          Invite model, data, options and inviter; it does not own a transport directly.
          Acceptance receives models explicitly from the retained instance; token codecs stay
          directly imported from the security library. Each call decodes the first
          invitation token, opens a transaction, locks a sent invite, checks expiry and existing
          user, deletes the invite and creates the user in that order. Email and role come from
          the stored invite; name and password come from the request. No invocation state is
          retained. Root units initialize fresh roots, assert no rendering or sending, and drive add
          through its actual collaborators. Acceptance units stub the transaction and model
          operations, asserting decoded sent-token lookup, shared transaction/internal/locking
          options and the stored email/role as authority for user-creation arguments.
        sources:
          - core/boot.js
          - core/server/services/invites/index.ts
          - core/server/services/invites/invites.ts
          - core/server/lib/mail/index.js
          - core/server/services/invites/accept.ts
          - core/server/models/base/index.js
          - core/server/models/base/plugins/events.js
          - core/server/models/user.js
        tests:
          - test/unit/server/services/invites/accept.test.ts
          - test/unit/server/services/invites/index.test.ts
      consumers:
        summary: >-
          The invites endpoint retains the default root and reads service.add during POST;
          browse, read and delete use models directly. Validation
          rejects an existing user and model permissions enforce role and staff limits.
          HTTP tests cover staff and integration-key invitation flows through real composition,
          stubbing GhostMailer.send; they assert send invocation, not rendered message contents.
          Root units separately assert staff/integration template data and the exact API-mail
          payload, with controlled model and rendering collaborators.
          The authentication endpoint reads service.accept from the retained root after input
          validation and setup checks; that method forwards to accept.ts with the same models.
          Its GET invitation lookup queries models directly. Acceptance units import the
          function with explicit models; legacy HTTP cases exercise the root's delegation
          through the real endpoint, asserting
          missing-invite 404, duplicate-user 422 and successful 200 responses.
        sources:
          - core/server/api/endpoints/invites.js
          - core/server/api/endpoints/utils/validators/input/invites.js
          - core/server/models/invite.js
          - core/server/api/endpoints/authentication.js
          - core/server/web/api/endpoints/admin/routes.js
          - core/server/api/endpoints/utils/validators/input/invitations.js
        tests:
          - test/e2e-api/admin/invites.test.js
          - test/e2e-api/admin/host-limits.test.ts
          - test/unit/server/models/invite.test.js
          - test/unit/server/services/invites/accept.test.ts
          - test/legacy/api/admin/authentication.test.js
          - test/unit/server/services/invites/index.test.ts
          - test/integration/services/invites/acceptance-models.test.ts
      startup:
        summary: >-
          Boot awaits init in initCore after settings and database readiness, before API and
          Express app loading. Consumers import the root early but read service during a request;
          reading it before successful initialization throws. Construction only captures borrowed
          collaborators. Calls still need the API pipelines ready; mail sending also needs the
          email-address service. The API mail handler lazily constructs its own shared mailer
          on first send. Real HTTP tests run after Ghost boot and replace send entirely, so
          do not establish the email-address/transport boundary or direct pre-boot invocation.
          Acceptance shares the same boot-created instance and starts its transaction only when
          invoked. HTTP requires completed setup and nonempty token, password and name; direct
          callers bypass those API checks. Existing HTTP cases exercise real boot and acceptance;
          generic kernel tests cover the readiness guard rather than direct setup-disabled acceptance.
        sources:
          - core/kernel/define-service.ts
          - core/boot.js
          - core/server/services/invites/index.ts
          - core/server/api/endpoints/mail.js
          - core/server/lib/mail/ghost-mailer.js
          - core/server/services/invites/accept.ts
          - core/server/api/endpoints/authentication.js
          - core/server/api/endpoints/utils/validators/input/invitations.js
        tests:
          - test/unit/kernel/define-service.test.ts
          - test/e2e-api/admin/invites.test.js
          - test/legacy/api/admin/authentication.test.js
      lifetime:
        summary: >-
          Successful init retains one instance and its dependency references; repeated init calls
          keep it rather than resetting operation state. There is no new shutdown/reset hook. Each add
          keeps its invite and email data local. Resending first destroys the previous invite
          with that email, then creates a replacement; the workflow has no transaction,
          per-email concurrency guard or retry. API mail independently retains its mailer
          across calls with no reset. Existing tests reuse an email for successive role
          invitations but do not assert old-token invalidation or concurrent resend behavior.
          Acceptance keeps its transaction, invite and user variables invocation-local.
          Persisted invite deletion provides the successful-use boundary. It has no reset,
          replay cache or retry. Database tests exercise the retained root against real models
          and assert concurrent acceptance, token reuse and failed creation leaving the invite
          available.
        sources:
          - core/kernel/define-service.ts
          - core/server/services/invites/invites.ts
          - core/server/api/endpoints/mail.js
          - core/server/services/invites/accept.ts
        tests:
          - test/unit/kernel/define-service.test.ts
          - test/e2e-api/admin/invites.test.js
          - test/unit/server/services/invites/index.test.ts
          - test/integration/services/invites/acceptance-models.test.ts
      configuration:
        summary: >-
          add reads current title, admin URL and default email domain. Inviter name/email
          selects the staff or integration template, reply-to and subject; signup links use
          the encoded stored token. Templates are read per render. Invite creation reads
          current db_hash and assigns one-week expiry. API-mail transport configuration is
          captured when its mailer is constructed; sender policy stays live in GhostMailer.
          Acceptance reads the current clock and stored expiry on invocation; only expires
          strictly less than Date.now is rejected. It looks up the decoded token and sent status
          without regenerating or checking a token signature. Database configuration, password
          policy and user defaults belong to the models; the action has no feature flag or
          configuration snapshot. Acceptance units pin URL-safe token decoding, rejection one
          millisecond after expiry and acceptance at the exact expiry boundary using a fixed clock.
        sources:
          - core/server/services/invites/invites.ts
          - core/server/models/invite.js
          - core/server/lib/mail/email-content-generator.js
          - core/server/lib/mail/ghost-mailer.js
          - core/server/api/endpoints/mail.js
          - core/server/services/invites/accept.ts
          - core/server/models/user.js
          - core/server/data/db/index.js
        tests:
          - test/unit/server/services/invites/index.test.ts
          - test/unit/server/services/invites/accept.test.ts
      resources:
        summary: >-
          The root borrows models, template filesystem work and API-mail sending. It awaits
          delete, create, render, send and the final sent-status edit sequentially. It owns
          no timers, subscriptions or cleanup hook. The nested API-mail handler owns the
          transport and can launch an unawaited warning notification on direct-mail failure;
          neither service supplies explicit transport cleanup or outstanding-send drainage.
          Acceptance borrows the shared database for one transaction and row locks; the
          transaction owns release. User creation and role attachment receive that transaction,
          and model events are deferred until commit. It adds no timers, subscriptions,
          transport or drain hook. Units assert internal context, the same transaction and
          forUpdate on invite/user lookup, invite deletion and user creation. Held promises
          prove deletion finishes before user creation and acceptance waits for creation.
          They do not test actual rollback, acquired locks or committed event delivery.
        sources:
          - core/server/services/invites/invites.ts
          - core/server/api/endpoints/mail.js
          - core/server/lib/mail/ghost-mailer.js
          - core/server/lib/mail/email-content-generator.js
          - core/server/ghost-server.ts
          - core/server/services/invites/accept.ts
          - core/server/models/user.js
          - core/server/models/base/bookshelf.js
          - core/server/models/base/plugins/events.js
        tests:
          - test/unit/server/services/invites/accept.test.ts
          - test/unit/server/services/invites/index.test.ts
      failures:
        summary: >-
          Model, rendering and mail failures reject add. EmailError gets a resend-help suffix
          and warning log on the same error before rejection. Destruction or creation is not
          rolled back after later failure; failed mail in the normal API flow leaves the replacement pending, while
          a sent-status edit can fail after mail was delivered. An unbound add rejects inside
          the chain after persistence, when it first reads this. Malformed arguments or a
          synchronously throwing model lookup can fail before the catch. No retry or compensation exists.
          Acceptance rejects missing sent invites and expired invites with NotFoundError, and
          existing users with ValidationError. Decode, database and user-validation errors
          propagate; transaction failures reject without a catch or retry. Accessing service before
          initialization throws. The initializer propagates construction failures and allows an
          explicit retry; it adds no operation retry. Those generic semantics are covered by kernel
          tests. HTTP cases assert
          missing-invite 404, duplicate-user 422 and successful 200. Acceptance units also
          assert missing/expired/duplicate errors, exact user-creation arguments and original
          rejection propagation from lookup, deletion, creation and transaction start/completion,
          with no subsequent model steps after failure. Malformed input, persisted rollback
          after deletion, emitted events and persisted staff-role assignment remain untested here.
        sources:
          - core/kernel/define-service.ts
          - core/server/services/invites/invites.ts
          - core/server/models/invite.js
          - core/server/api/endpoints/mail.js
          - core/server/services/invites/accept.ts
          - core/server/models/base/index.js
          - core/server/api/endpoints/utils/validators/input/invitations.js
        tests:
          - test/unit/kernel/define-service.test.ts
          - test/legacy/api/admin/authentication.test.js
          - test/unit/server/services/invites/index.test.ts
          - test/unit/server/services/invites/accept.test.ts
    unverified: []
    blockers:
      - >-
        Characterize resend and partial failures with real persistence: existing invite
        deletion, replacement creation, template failure, mail rejection and post-send status
        failure. Include unbound invocation, which can mutate persistence before rejecting.
        Define concurrency/retry expectations before changing ownership.
      - >-
        Assert mail recipient, subject, reply-to, HTML/text and signup URL through the real
        HTTP consumer for staff and integration invites. Root units now pin both payloads and
        live settings read through the same imported instance across calls, with controlled
        models/rendering/API mail; they do not establish real provider delivery.
      - >-
        Resolve API-mail orchestration and transport ownership without moving business logic
        into HTTP. Preserve role/limit checks and successful behavior while introducing boot
        composition; test readiness and shutdown of pending work separately.
      - >-
        Add real-database contracts for rollback when user creation or role attachment fails,
        concurrent acceptance, token reuse and committed model events. Units now pin all model
        options, awaited deletion/creation and failure propagation, but stub the transaction;
        they do not establish persisted rollback or acquired locks.
      - >-
        Extend HTTP acceptance assertions beyond the response to the persisted user, staff role
        and password behavior. Units now pin decoded sent-token lookup, stored email/role
        authority, forwarded name/password and the expiry boundary without real persistence.
  jobs-service:
    kind: directory
    disposition: split
    status: audited
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: complex
      modelImpact: may-change
      reason: >-
        Initialization intentionally reuses the same object but clears handlers on every
        boot; consumers retain it through shutdown, then boot registers handlers and starts
        its backend again. Decide how one-time construction relates to per-boot reset,
        readiness and restart. A memoized init alone would skip required reset, while
        replacing the object would strand retained consumers.
      reviewedAt: f276513ba32b7114fd5cc4f7b02d3f82c0c31e3e
      sources:
        - core/server/services/jobs-service/index.ts
        - core/server/services/jobs-service/jobs-service.ts
        - core/boot.js
        - test/unit/server/services/jobs-service/index.test.js
        - test/unit/server/services/jobs-service/retained-consumer.test.ts
      dependencies:
        complexity: complex
        reason: >-
          Queue/backend, logging, reporting and outbox dependencies are mostly injected, but
          register-job-handlers imports job classes across services and directly reaches Content
          Import, Update Check and Signing Keys. It is application composition inside the queue
          implementation, and registration must occur after its service dependencies exist.
        preparation:
          - >-
            Move cross-service handler registration to application boot composition, keeping
            queue-specific implementation independent of other service roots.
          - >-
            Inject cron validation and after-commit scheduling into JobsService; preserve
            transaction boundaries and registration-before-start on the retained queue.
          - >-
            Keep per-boot handler reset and activation separate from one-time dependency
            construction.
        reviewedAt: 44ad405bff4a8700af2ed51f833ebc08d25dddff
        sources:
          - core/server/services/jobs-service/index.ts
          - core/server/services/jobs-service/jobs-service.ts
          - core/server/services/jobs-service/register-job-handlers.ts
          - core/server/services/jobs-service/jobs-outbox.ts
          - core/server/services/jobs-service/outbox-relay.ts
          - core/boot.js
    entryPoints:
      - jobs-service/index.ts
      - jobs-service/register-job-handlers.ts
    notes: >-
      Planned split and relocation out of services: generic scheduling, dispatch, execution
      and outbox machinery belong in kernel; application configuration, registration and
      activation belong in boot; job definitions and handlers belong with their owning
      services. The kernel boundary must exclude Ghost's config/schema imports and concrete
      database wiring, including outbox availability checks and the jobs_outbox mapping. No
      source has moved. Reassess initialization after separating ownership rather than
      migrating this root in place. Preserve the retained process instance, per-boot handler
      reset, registration-before-start and backend-specific shutdown. Relocation does not
      remove the queue's dependency, readiness or lifetime contracts.
    contracts:
      exports:
        summary: >-
          Named TypeScript init/getInstance/shutdown exports. getInstance throws before
          initialization; shutdown before initialization resolves without constructing a
          service. Boot separately imports register-job-handlers to wire domain handlers.
        sources:
          - core/server/services/jobs-service/index.ts
          - core/server/services/jobs-service/register-job-handlers.ts
          - core/boot.js
        tests:
          - test/unit/server/services/jobs-service/index.test.js
      construction:
        summary: >-
          The first init constructs JobsService with the configured jobs adapter. Subsequent
          init calls clear handler and queue declarations and reuse the same service and
          backend rather than looking up a new adapter.
        sources:
          - core/server/services/jobs-service/index.ts
          - core/server/services/jobs-service/jobs-service.ts
        tests:
          - test/unit/server/services/jobs-service/index.test.js
          - test/unit/server/services/jobs-service/jobs-service.test.ts
      startup:
        summary: >-
          Boot registers handlers after asynchronous service prerequisites settle, awaits
          jobs.start, then starts automation polling.
        sources:
          - core/boot.js
          - core/server/services/jobs-service/jobs-service.ts
        tests:
          - test/unit/boot/init-services.test.ts
          - test/e2e-server/boot-readiness.isolated.test.ts
      lifetime:
        summary: >-
          The wrapper retains its jobs instance through shutdown. MentionController
          retains the injected instance and calls dispatch; reinitialization clears
          handlers and queue declarations before registration.
        sources:
          - core/server/services/jobs-service/index.ts
          - core/server/services/jobs-service/jobs-service.ts
          - core/server/services/mentions/mention-controller.js
        tests:
          - test/unit/server/services/jobs-service/index.test.js
          - test/unit/server/services/jobs-service/jobs-service.test.ts
          - test/unit/server/services/jobs-service/retained-consumer.test.ts
      consumers:
        summary: >-
          Source inspection establishes these callers: boot injects the instance into
          retained mentions and email consumers and a fresh site importer. Members, content CSV imports, the media API and email analytics
          resolve it when dispatching or scheduling. Gift/member cleanup, signing keys,
          update checks and Tinybird schedule through boot-supplied references. The default
          backend runs jobs in-process; registered handlers call initialized domain services.
          The cited tests cover dispatch through a retained MentionController across restart
          and handler registration. The other consumer wiring is source-only evidence here.
        sources:
          - core/boot.js
          - core/server/adapters/jobs/InMemoryJobsBackend.ts
          - core/server/services/jobs-service/register-job-handlers.ts
          - core/server/services/mentions/mention-controller.js
          - core/server/services/mentions/mention-sending-service.js
          - core/server/services/email-service/batch-sending-service.js
          - core/server/data/importer/import-manager.js
          - core/server/data/importer/index.js
          - core/server/services/members/service.js
          - core/server/services/content-import/index.ts
          - core/server/api/endpoints/db.js
          - core/server/services/email-analytics/jobs/index.js
          - core/server/services/email-analytics/jobs/email-analytics-job-scheduler.ts
          - core/server/services/gifts/jobs/index.js
          - core/server/services/members/jobs/index.js
          - core/server/services/signing-keys/index.ts
          - core/server/services/update-check/index.js
          - core/server/services/tinybird-sync/tinybird-sync-service.ts
        tests:
          - test/unit/server/services/jobs-service/register-job-handlers.test.ts
          - test/unit/server/services/jobs-service/retained-consumer.test.ts
      configuration:
        summary: >-
          The wrapper looks up the jobs adapter on first init; reinit reuses the same
          service and backend. Checked-in defaults select InMemoryJobsBackend with
          concurrency 3. Registration declares separate queue limits. Boot passes
          server:shutdownTimeout to jobs shutdown when its cleanup task runs; the in-memory
          backend otherwise waits 10 seconds.
        sources:
          - core/server/services/jobs-service/index.ts
          - core/server/services/jobs-service/register-job-handlers.ts
          - core/shared/config/defaults.json
          - core/server/adapters/jobs/InMemoryJobsBackend.ts
          - core/boot.js
        tests:
          - test/unit/server/services/jobs-service/index.test.js
          - test/unit/server/services/jobs-service/register-job-handlers.test.ts
          - test/unit/server/adapters/jobs/in-memory-jobs-backend.test.ts
          - test/e2e-server/boot-readiness.isolated.test.ts
      resources:
        summary: >-
          The in-memory backend reads recurring schedules in local time and keeps the first
          registration per job type. Shutdown stops accepting work, clears recurring timers,
          drops queued work and bounds the wait for active deliveries, then discards its
          queues so a re-boot starts with empty lanes. Enqueue or schedule before start
          throws; after shutdown both are dropped. A passed deadline does not cancel active
          deliveries, and an early drain leaves its deadline timer pending. GhostServer
          drains HTTP before starting its concurrent cleanup tasks, and jobs shut down in a
          cleanup task after the HTTP drain.
        sources:
          - core/server/adapters/jobs/InMemoryJobsBackend.ts
          - core/server/services/jobs-service/jobs-service.ts
          - core/boot.js
          - core/server/ghost-server.ts
        tests:
          - test/unit/server/adapters/jobs/in-memory-jobs-backend.test.ts
          - test/unit/server/services/jobs-service/jobs-service.test.ts
          - test/unit/server/ghost-server.test.ts
          - test/e2e-server/boot-readiness.isolated.test.ts
      failures:
        summary: >-
          Invalid declarations, duplicate handler types and conflicting queue limits fail.
          Dispatch and recurring scheduling propagate payload serialization, validation
          and backend errors.
          Deliveries for unknown job types are logged and dropped. Handler errors are
          logged, reported and rethrown to the backend; the in-memory backend logs them and
          continues other deliveries without retrying. A recurring tick whose enqueue throws
          is logged and the schedule keeps running. A real-boot test rejects jobs.start with a
          Ghost error and asserts the same failed-readiness error, listener closure at the
          requested exit and exit code 2; process.exit is stubbed. It does not cover plain-error
          wrapping, every cleanup task before exit or all boot failure stages.
        sources:
          - core/server/ghost-server.ts
          - core/boot.js
          - core/server/services/jobs-service/jobs-service.ts
          - core/server/adapters/jobs/InMemoryJobsBackend.ts
        tests:
          - test/e2e-server/boot-failure.isolated.test.ts
          - test/unit/server/services/jobs-service/jobs-service.test.ts
          - test/unit/server/adapters/jobs/in-memory-jobs-backend.test.ts
    unverified: []
    blockers:
      - >-
        Before changing consumer wiring, add contract tests for the boot-injected email
        and site-importer references, dispatch-time lookups and boot-supplied scheduling
        references identified above. The cited consumer tests do not cover these paths.
        Custom jobs adapters need their own evidence.
      - >-
        Recurring scheduling guards in gifts, members, signing keys, email analytics and
        Tinybird survive backend shutdown, which removes their timers. Recurrence after
        in-process restart is not established; resolve this legacy limitation explicitly.
      - >-
        Timed-out handlers can outlive cleanup, and an early drain leaves its deadline
        timer pending. Do not claim cancellation or safe dependency disposal from bounded
        shutdown alone. Domain handlers such as the site importer may absorb their own
        failures, so settlement is not proof of successful domain work. Cross-service drain
        ordering and failures outside the tested jobs-start boundary remain open.
  limits.ts:
    kind: file
    disposition: migrate
    futureHome: core/shared/limits
    status: audited
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: complex
      modelImpact: fits
      reason: >-
        Models retain the eager policy instance and boot loads it before settings, so
        construction and consumer ordering need coordinated changes. Keep policy reload as
        an explicit operation on that retained instance and preserve tolerated
        invalid-configuration behavior. These are boundary and error-policy decisions;
        mutable policy does not require replacing the service or changing the helper.
      reviewedAt: f276513ba32b7114fd5cc4f7b02d3f82c0c31e3e
      sources:
        - core/server/services/limits.ts
        - core/server/models/integration.js
        - core/boot.js
        - test/utils/host-limits-utils.ts
        - test/unit/server/services/limits.test.js
      dependencies:
        complexity: straightforward
        reason: >-
          This file is already the composition point for LimitService, config, database and logging.
          A root index.ts can keep that wiring together; the main dependency constraint is making
          model and route consumers obtain the initialized instance after limits boot, rather than
          evaluating its guarded getter during import.
        preparation:
          - >-
            Move the composition into a service root and update eager model/API bindings to use the
            ready policy instance at operation time, retaining boot-before-settings order and live
            policy reload.
        reviewedAt: 44ad405bff4a8700af2ed51f833ebc08d25dddff
        sources:
          - core/server/services/limits.ts
          - core/server/models/integration.js
          - core/server/models/user.js
          - core/server/models/invite.js
          - core/server/services/members-metafields/index.ts
          - core/boot.js
    entryPoints:
      - limits.ts
    notes: >-
      Suggested future home after this migration: Ghost-wide entitlement/quota policy belongs in
      core/shared/limits. The reusable limit engine stays in @tryghost/limit-service. Boot
      injects configuration and database dependencies so Shared does not import server code. No
      source has moved. Boot loads host policy into an eagerly exported, widely retained
      instance. Preserve per-consumer enforcement and existing configuration quirks.
    contracts:
      exports:
        summary: >-
          Named TypeScript exports are limitService and synchronous receiver-independent init
          returning undefined. Callers use the instance's synchronous isLimited/isDisabled, async
          throwing and boolean checks, and public limits map; the theme installer reads an
          allowlist directly. Package instance methods depend on their receiver. No test pins
          the root's exact export or receiver surface.
        sources:
          - core/server/services/limits.ts
          - core/server/services/themes/installer.js
        tests:
          - test/e2e-api/admin/host-limits.test.ts
      construction:
        summary: >-
          Import constructs one empty LimitService from the workspace package. Every init
          reloads that same object with current host limits, subscription, help link, the
          shared db module and Ghost errors. There is no initialized guard, promise or
          configurable factory. Loading synchronously constructs per-limit implementations;
          neither construction nor loading queries the database. Core init tests stub
          loadLimits and test error handling, without asserting its full argument object.
        sources:
          - core/server/services/limits.ts
          - core/server/data/db/index.js
        tests:
          - test/unit/server/services/limits.test.js
      consumers:
        summary: >-
          Models gate publishing, staff invites/reactivation/promotion and integration creation;
          imports become Contributors whenever staff is configured. API-key authentication
          exempts internal/core integrations, whereas limited webhooks retain only internal
          integrations. Retained dependencies in newsletters, email, settings and settings
          helpers govern active newsletters, send/retry checks, Stripe connection, private-site
          settings and social/analytics availability. Theme endpoints/installer and file/media
          validators enforce values/sizes. Custom fields' namespace policy asks isDisabled on
          every member metafield read and errorIfWouldGoOverLimit on every change to a field.
          HTTP assertions cover integration creation/authentication, newsletter add/reactivation,
          member-limit publishing, staff invitations, theme/file limits and member-field guards.
          Privacy, social/analytics, importer demotion and several user-model paths remain source-only.
        sources:
          - core/server/models/post.js
          - core/server/models/user.js
          - core/server/models/invite.js
          - core/server/models/integration.js
          - core/server/data/importer/importers/data/users-importer.js
          - core/server/auth/api-key/admin.js
          - core/server/auth/api-key/content.js
          - core/server/services/webhooks/index.js
          - core/server/services/webhooks/webhook-trigger.js
          - core/server/services/newsletters/index.ts
          - core/server/services/newsletters/newsletters-service.js
          - core/server/services/email-service/email-service-wrapper.js
          - core/server/services/email-service/email-service.js
          - core/server/services/settings/settings-service.js
          - core/server/services/settings/settings-bread-service.js
          - core/server/services/settings-helpers/index.js
          - core/server/services/settings-helpers/settings-helpers.js
          - core/server/services/themes/installer.js
          - core/server/api/endpoints/themes.js
          - core/server/api/endpoints/utils/validators/input/files.js
          - core/server/api/endpoints/utils/validators/input/media.js
          - core/server/services/members-metafields/index.ts
          - core/server/services/members-metafields/custom-fields.ts
        tests:
          - test/e2e-api/admin/host-limits.test.ts
          - test/e2e-api/admin/member-custom-fields.test.ts
          - test/e2e-api/admin/newsletters.test.js
          - test/e2e-api/admin/key-authentication.test.js
          - test/e2e-api/content/key-authentication.test.js
          - test/e2e-api/admin/posts-legacy.test.js
      startup:
        summary: >-
          initCore awaits init before settings initialization: settings enforce public-site
          privacy and derive social/analytics availability using loaded limits. The eager
          instance is callable before init and has no limits then. The root does not await a
          loadLimits return value; the current package method is synchronous. Boot-based
          authentication/newsletter/email tests exercise configured startup, but do not assert
          the ordering or pre-init contract explicitly. Public-site startup tests stub the
          limit method, so they do not prove real configuration reaches that consumer.
        sources:
          - core/boot.js
          - core/server/services/limits.ts
          - core/server/services/settings/settings-service.js
          - core/server/services/settings-helpers/settings-helpers.js
        tests:
          - test/e2e-api/admin/key-authentication.test.js
          - test/e2e-api/content/key-authentication.test.js
          - test/e2e-api/admin/newsletters.test.js
          - test/e2e-api/admin/posts-legacy.test.js
          - test/unit/server/services/settings/settings-service.test.js
      lifetime:
        summary: >-
          Module caching retains the exported instance and callers' references across init.
          Each load replaces its limits map and creates new limit objects, rather than
          updating old ones; an already-started check can finish against its former object.
          There is no automatic host-config subscription, reset/stop, query cancellation or
          cross-request reservation. Core host-limit helpers explicitly call init after
          changing/restoring config, and tests assert removal of a prior limit. Retained
          references, concurrent checks/reloads and restart identity are source-only findings.
        sources:
          - core/server/services/limits.ts
          - core/server/services/newsletters/index.ts
          - core/server/services/settings-helpers/index.js
          - core/server/services/email-service/email-service-wrapper.js
          - core/server/services/themes/installer.js
        tests:
          - test/e2e-api/admin/host-limits.test.ts
      configuration:
        summary: >-
          init uses hostSettings:limits or {}, a billing URL only when enabled is boolean true,
          otherwise https://ghost.org/help/, and a truthy subscription object supplies start
          with interval fixed to month. Package loading recognizes configured names and chooses
          allowlist, max, maxPeriodic, then flag by property presence. Unknown names are ignored;
          noncanonical names can load without their supplied settings. Flags use truthiness,
          so string false disables too. Scalar policy is captured on load, allowlist arrays
          are retained, and counts/current monthly period are evaluated per check. API config
          separately exposes host configuration, not proof a limit loaded. Tests pin several
          quirks and billing-enabled output, not every default or configuration boundary.
        sources:
          - core/server/services/limits.ts
          - core/server/api/endpoints/config.js
          - core/server/api/endpoints/utils/public-config/config.js
          - core/server/services/settings/settings-service.js
        tests:
          - test/e2e-api/admin/host-limits.test.ts
      resources:
        summary: >-
          The root and its limit objects retain policy/dependency references and own no timers,
          subscriptions or transport. Count checks borrow the shared lazy knex connection or
          supplied transaction and await queries; newsletters forwards transaction context for
          add/reactivation. Counts cover members, active newsletters, email totals since the
          current billing-period start, and non-Contributor staff excluding inactive users plus
          non-Contributor invites. Uploads supply
          bytes instead of querying. No counter cache, lock, reservation, shutdown or drain
          is supplied. HTTP tests exercise database-backed refusals with/without newsletter
          transactions, without proving concurrent capacity or complete SQL count semantics.
        sources:
          - core/server/services/limits.ts
          - core/server/data/db/index.js
          - core/server/data/db/connection.ts
          - core/server/services/newsletters/newsletters-service.js
          - core/server/services/email-service/email-service.js
          - core/server/api/endpoints/utils/validators/input/files.js
          - core/server/api/endpoints/utils/validators/input/media.js
        tests:
          - test/e2e-api/admin/host-limits.test.ts
          - test/e2e-api/admin/newsletters.test.js
      failures:
        summary: >-
          Only synchronous loadLimits IncorrectUsageError is warned and swallowed; other
          synchronous errors escape unchanged. Config reads occur outside that catch. Loading
          resets the map before building entries, so failure retains any successful prefix and
          omits the failed entry and following entries. Core tests assert exact warning/error
          identity, and HTTP tests pin a first-entry failure leaving subsequent policy absent.
          Invalid nonempty dates can register periodic limits; that test does not exercise
          their query. Check failures reject callers; boolean checks convert only HostLimitError
          to true. HTTP middleware forwards errors, endpoint wrappers can reword them, and
          webhook preparation logs a failed check and sends nothing. There is no retry or rollback.
        sources:
          - core/server/services/limits.ts
          - core/server/services/webhooks/webhook-trigger.js
          - core/server/auth/api-key/content.js
          - core/server/api/endpoints/newsletters.js
          - core/server/api/endpoints/settings.js
        tests:
          - test/unit/server/services/limits.test.js
          - test/e2e-api/admin/host-limits.test.ts
          - test/e2e-api/admin/member-custom-fields.test.ts
          - test/unit/server/services/newsletters/service.test.js
          - test/unit/server/services/email-service/email-service.test.js
    unverified: []
    blockers:
      - >-
        Pin root construction/export identity, pre-init behavior, exact load arguments and boot
        ordering, including strict billing-enabled/default help and subscription defaults.
        Characterize retained callers, the public theme allowlist and in-flight checks across
        reload/restart before replacing the shared instance or map.
      - >-
        Characterize failed reload after earlier valid entries and recovery, config-read failure
        and query rejection. First-entry failure tests do not prove atomic load or rollback.
        Treat string false, noncanonical names, invalid periodic dates and partial loading as
        recorded compatibility hazards; fix them separately from initialization if desired.
      - >-
        Extend real-root consumer assertions for staff reactivation/promotion/import, integration
        creation, public-site/social/analytics policy, media/thumbnail uploads, theme-install
        exceptions and webhook filtering. Existing helper, webhook and theme-install tests use
        injected/stubbed limits; add targeted tests for the retained production compositions.
      - >-
        Verify counted-query boundaries, periodic history and recipient addedCount through real
        consumers, plus retry/resume differences and concurrent capacity decisions. A max-zero
        refusal and package query stubs do not prove date filtering, staff exclusions or safe
        concurrent check-then-write behavior. Initialization must keep borrowing database/transaction
        ownership without adding untested cancellation or cleanup behavior.
      - >-
        Keep host-policy business logic outside HTTP; moving the root wholesale into API would
        conflict with the placement rules. Recorded facets do not establish migration readiness.
  link-redirection:
    kind: directory
    disposition: migrate
    migrated: true
    extras:
      linkRedirectRepository:
        purpose: Hand the ready repository to link tracking, which rebuilds post links from redirect rows, and to newsletter integration tests.
        callers:
          - core/server/services/link-tracking/index.js
          - test/integration/services/email-service/batch-sending.test.js
        timing: >-
          Available once the factory has run, which happens synchronously inside the first
          init() call and so before init resolves; until then it throws the same readiness
          error as the service getter. Boot awaits init before link tracking's init reads it.
        dependencies:
          - The repository constructed by the factory, including its optional cache adapter and site.changed listener.
        failurePolicy: Throws IncorrectUsageError before initialization; after init it only returns the retained instance and never constructs.
        destination: Retire by narrowing link tracking to the service API or moving post-link reconstruction behind a service method.
    status: audited
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: straightforward
      modelImpact: fits
      reason: >-
        The boot-owned default root returns one ready LinkRedirectsService after adapters and
        models and before link tracking. The repository link tracking borrows is published
        through a guarded transitional extras accessor rather than a public field. The borrowed
        cache, its site.changed listener and per-request redirect behavior are unchanged; no new
        kernel lifecycle is needed.
      reviewedAt: 9687efed5b3180f97e35078367822db16aa1c152
      sources:
        - core/server/services/link-redirection/index.ts
        - core/server/services/link-redirection/link-redirect-repository.js
        - core/server/services/link-tracking/index.js
        - core/boot.js
        - test/unit/server/services/link-redirection/index.test.js
      dependencies:
        complexity: straightforward
        reason: >-
          The root composes models, URL utilities, the optional cache, the shared event bus and
          the domain event dispatcher in index.ts; the implementation receives the dispatcher
          instead of requiring it. The repository keeps its allowlisted debug tracing. Link
          tracking still borrows the repository through the transitional accessor and
          deep-imports redirect-event; narrowing it to the service API is a separate change.
        preparation: []
        reviewedAt: 9687efed5b3180f97e35078367822db16aa1c152
        sources:
          - core/server/services/link-redirection/index.ts
          - core/server/services/link-redirection/link-redirects-service.js
          - core/server/services/link-redirection/link-redirect-repository.js
          - core/server/services/link-tracking/index.js
          - core/server/services/link-tracking/link-click-tracking-service.js
          - core/boot.js
    entryPoints:
      - link-redirection/index.ts
    notes: >-
      Preserve frontend redirects, optional cache behavior and initialization before link
      tracking. The prior auditedAt records the pre-migration baseline. The bounded migration
      was inspected on main 9687efed5b3180f97e35078367822db16aa1c152; root, repository, service and link
      tracking units and the automation integration passed before production changes. The
      default initializer replaces the wrapper, and the repository reach-through used by link
      tracking is a transitional extras accessor with the same readiness guard. The cache
      serialization round trip on subdirectory sites is unchanged and stays recorded below for a
      separate fix.
    contracts:
      exports:
        summary: >-
          The TypeScript root default-exports a defineService initializer with init, a guarded
          service getter and one extras accessor, linkRedirectRepository(), which throws the
          same readiness error before init. The ready service exposes async slug/create/filter/
          request operations and a synchronous relativeRedirectPrefix; only handleRequest is
          bound. Deep LinkRedirect objects retain mutable URL references and ObjectIDs, while
          RedirectEvent carries the original request URL, link and timestamp. Root units pin both
          readiness guards, retained identity across repeated init and the published
          handleRequest and getSlugUrl paths; generic shared initialization and retry belong to
          kernel tests.
        sources:
          - core/kernel/define-service.ts
          - core/server/services/link-redirection/index.ts
          - core/server/services/link-redirection/link-redirects-service.js
          - core/server/services/link-redirection/link-redirect.ts
          - core/server/services/link-redirection/redirect-event.js
        tests:
          - test/unit/server/services/link-redirection/link-redirects-service.test.js
          - test/unit/server/services/link-redirection/link-redirect-repository.test.js
          - test/unit/server/services/link-redirection/index.test.js
      construction:
        summary: >-
          Import declares the root without resolving runtime collaborators. The factory requires
          config, URL utilities, the adapter manager, the shared event registry, the domain event
          dispatcher, models and the implementation classes, constructs the repository with an
          optional cache:linkRedirectsPublic adapter, then the service with the site URL, and
          publishes the repository to the accessor only after both exist. Publication follows the
          awaited factory, so consumers await init rather than relying on synchronous
          publication. Cache-enabled repository construction immediately adds a site.changed
          listener; a later site URL failure would leave that listener registered and retry adds
          another. Root units pin graph retention, disabled-cache construction without an adapter
          or listener, and adapter-acquisition failure leaving both guards closed before an
          explicit retry succeeds.
        sources:
          - core/kernel/define-service.ts
          - core/server/services/link-redirection/index.ts
          - core/server/services/link-redirection/link-redirect-repository.js
          - core/server/services/adapter-manager/index.ts
        tests:
          - test/unit/server/services/link-redirection/index.test.js
      consumers:
        summary: >-
          Frontend route mounting reads the /r/ prefix and captures the bound handleRequest from
          the ready service. Link tracking reads the service getter and the transitional
          extras.linkRedirectRepository() accessor and retains both for newsletter and automation
          link creation, click events, projections and updates. Its post repository uses the
          borrowed repository to reconstruct rows, and its click service deep-imports
          redirect-event. Newsletter integration reads the repository through the same accessor
          after boot and proves generated links exist in the database and resolve through real
          frontend HTTP to the stored destination; its nonempty click assertion does not pin the
          individual relation because supplied member_id/link_id options are ignored. Link
          tracking units stub the getter and accessor; none prove retained consumers after root
          replacement. Root units invoke the published handleRequest with request/response fakes
          and a stateful cache fake: a database miss and a serialized cache hit assert the same
          destination, header, no next call and the same link id, automation revision and
          destination in the dispatched event. Models and URL helpers are controlled, so this is
          not a real HTTP test.
        sources:
          - core/frontend/web/routers/link-redirects.js
          - core/frontend/web/site.js
          - core/server/services/link-tracking/index.js
          - core/server/services/link-tracking/link-click-tracking-service.js
          - core/server/services/link-tracking/post-link-repository.js
          - core/server/services/link-tracking/link-click-repository.js
          - core/server/models/base/plugins/sanitize.js
          - core/server/models/base/plugins/crud.js
        tests:
          - test/integration/services/email-service/batch-sending.test.js
          - test/unit/server/services/link-tracking/index.test.ts
          - test/unit/server/services/link-redirection/index.test.js
      startup:
        summary: >-
          Boot awaits this init after route/custom redirects and before themes, Express route
          mounting, the main services phase and readiness. Despite the frontend-phase name,
          initialization also runs when frontend is disabled. Link tracking reads the guarded
          getter and accessor, so an uninitialized root rejects its init with the readiness
          error. No redirect rows are loaded during initialization. Successful newsletter
          composition exercises completed boot. Root units cover failed adapter acquisition and
          retry; lifecycle tests do not cover this root's ordering, later repository
          construction failure or backend-only startup.
        sources:
          - core/boot.js
          - core/server/services/link-redirection/index.ts
          - core/frontend/web/routers/link-redirects.js
          - core/server/services/link-tracking/index.js
        tests:
          - test/integration/services/email-service/batch-sending.test.js
          - test/unit/server/services/link-tracking/index.test.ts
          - test/unit/server/services/link-redirection/index.test.js
      lifetime:
        summary: >-
          The root keeps its first successful graph across init calls and has no reset.
          Persistence outlives the graph; optional adapter caching stores positive results only.
          Clearing shared events leaves the guarded repository without its invalidator. Slugs
          use four random bytes and repeat lookup until unused, without an attempt limit or a
          transaction joining lookup/save; from has an index, not a unique constraint. Automation
          deduplication instead relies on a unique revision/destination-hash pair and rereads a
          concurrent winner after a duplicate error. Real MySQL tests prove convergence for
          concurrent same/different revisions, not ordinary slug races or root/cache lifetime.
          Root units pin retained service/repository identity after repeated init and one owned
          invalidation listener.
        sources:
          - core/server/services/link-redirection/index.ts
          - core/server/services/link-redirection/link-redirects-service.js
          - core/server/services/link-redirection/link-redirect-repository.js
          - core/server/data/schema/schema.js
        tests:
          - test/integration/services/link-redirection/automation-redirects.test.ts
          - test/unit/server/services/link-redirection/link-redirects-service.test.js
          - test/unit/server/services/link-redirection/index.test.js
      configuration:
        summary: >-
          Init captures cache enablement/adapter and service base URL; later init does not refresh
          them. Repository URL helpers run per operation but shared URL utilities freeze site,
          subdirectory and Admin URL values at construction; test replacement is not production
          hot reload. Persistence strips the subdirectory and request query. Destination hashes
          use full URL.href, and edited means updated_at exceeds created_at by more than one
          second. Cache serialization keeps a subdirectory-bearing pathname: save writes that
          pathname as its key while getByURL reads and fills the stripped path, and cached
          reconstruction prefixes the subdirectory again, so cache hits on subdirectory sites
          rebuild LinkRedirect.from with a doubled path while to is unchanged. Units pin root-URL
          cache results, timestamp thresholds and UUID substitution. Root units run a real
          serialization round trip through the root graph with subdirectory URL helpers, pinning
          destination and identifiers on miss and hit and slug URLs under the site URL; the
          doubled from value is deliberately not pinned.
        sources:
          - core/server/services/link-redirection/index.ts
          - core/server/services/link-redirection/link-redirect-repository.js
          - core/server/services/link-redirection/link-redirects-service.js
          - core/server/models/redirect.js
          - core/shared/url-utils.ts
          - core/shared/config/validated.ts
        tests:
          - test/unit/server/services/link-redirection/link-redirect-repository.test.js
          - test/unit/server/services/link-redirection/link-redirects-service.test.js
          - test/integration/services/link-redirection/automation-redirects.test.ts
          - test/unit/server/services/link-redirection/index.test.js
      resources:
        summary: >-
          The repository borrows models and an adapter-manager-owned cache, registering an
          anonymous site.changed listener when enabled; it owns no timer, pool or outbound
          request. Cache writes and resets are not awaited, and no listener disposal or pending
          operation drain exists. RedirectEvent dispatch through the injected dispatcher precedes
          destination substitution and HTTP 302; shared DomainEvents logs ordinary async
          subscriber failures without making them part of the response. Newsletter integration
          waits for test-only allSettled, while production redirect completion does not await
          analytics. The reset unit checks one invocation on an injected emitter, not resource
          ownership across restart. Root units assert one owned site.changed listener on the
          shared registry and cache reset through its callback; production disposal is still
          absent.
        sources:
          - core/server/services/link-redirection/index.ts
          - core/server/services/link-redirection/link-redirect-repository.js
          - core/server/services/link-redirection/link-redirects-service.js
          - core/server/lib/common/events.js
          - core/server/services/adapter-manager/index.ts
          - core/server/ghost-server.ts
          - package.json
        tests:
          - test/unit/server/services/link-redirection/link-redirect-repository.test.js
          - test/integration/services/email-service/batch-sending.test.js
          - test/unit/server/services/link-redirection/index.test.js
      failures:
        summary: >-
          Repository/cache lookup and malformed-row errors propagate without a cache-error DB
          fallback. A synchronous cache write/serialization error can reject after persistence;
          rejected cache write/reset promises are detached. handleRequest catches errors into
          next, and missing rows call next without redirecting. It sets noindex/nofollow and
          replaces or removes UUID placeholders only when present; request m must match the
          hexadecimal UUID shape. Automation duplicate-key recovery returns a reread winner or
          rethrows, while other saves fail directly. Units pin that recovery and placeholder
          output with fakes, not cache faults, malformed stored rows, analytics failures or
          Express error responses. No-prefix unit cases simply inject a missing row; they do
          not prove route matching or service-side prefix validation. A root unit forwards a
          model lookup rejection unchanged to next without redirect or event dispatch; cache
          failure channels and full Express error responses remain gaps.
        sources:
          - core/server/services/link-redirection/index.ts
          - core/server/services/link-redirection/link-redirect-repository.js
          - core/server/services/link-redirection/link-redirects-service.js
          - core/frontend/web/routers/link-redirects.js
          - package.json
        tests:
          - test/unit/server/services/link-redirection/link-redirects-service.test.js
          - test/unit/server/services/link-redirection/link-redirect-repository.test.js
          - test/unit/server/services/link-redirection/index.test.js
    unverified: []
    blockers:
      - >-
        Root units pin both readiness guards, repeated-init identity and failed adapter
        acquisition with retry. Pin retained frontend/link tracking consumers after root
        replacement and backend-only boot. Decide how to restore invalidation after an event-bus
        reset and dispose partially constructed repositories without changing identity. Retire
        the transitional repository accessor by narrowing link tracking to the service API.
      - >-
        Root units cover disabled caching, a stateful miss-then-hit round trip, destination and
        event identifiers, retained adapter choice and site.changed invalidation with controlled
        subdirectory URL helpers. Serialized cache entries rebuild from against a site URL that
        already carries the subdirectory, so cache hits on subdirectory sites yield a doubled
        path, and the save-time key differs from the lookup key; current consumers do not read
        that field from a hit. Fix that round trip separately rather than pinning it. Cover
        edits and cache-enabled races separately.
      - >-
        Root units cover a model lookup failure reaching next without redirecting. Cover cache
        read/write/reset failure channels, malformed model/cache rows, partial persistence and
        full Express error responses. Cache writes and invalidation are not awaited, so request
        success is not proof of settled cache work or safe shutdown.
      - >-
        Keep tested automation revision/hash uniqueness distinct from unprotected ordinary
        slug lookup/save. Test ordinary concurrent collisions, partial automation saves and
        cache-enabled races; existing real concurrency coverage uses caching disabled.
      - >-
        Strengthen redirect-event and newsletter click assertions to prove exact link/member
        delivery and behavior when analytics fails. Cover frontend subdirectory matching,
        query/placeholder encoding and unrelated encoded destination components using the real
        HTTP boundary; current method fakes do not establish those composition contracts.
  link-tracking:
    kind: directory
    disposition: migrate
    status: audited
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: complex
      modelImpact: fits
      reason: >-
        Shared initialization and retry already match defineService, but the graph exposes a
        repository as well as the service, depends on redirection, and registers an
        anonymous wrapped DomainEvents subscriber. Establish targeted rollback/disposal and
        remove renderer-owned initialization while preserving newsletter and automation
        consumers. The ready-instance model still fits; event-subscription ownership needs
        implementation work.
      reviewedAt: f276513ba32b7114fd5cc4f7b02d3f82c0c31e3e
      sources:
        - core/server/services/link-tracking/index.js
        - core/server/services/link-tracking/link-click-tracking-service.js
        - core/server/services/member-welcome-emails/member-welcome-email-renderer.js
        - core/boot.js
        - test/unit/server/services/link-tracking/index.test.ts
      dependencies:
        complexity: straightforward
        reason: >-
          Models, transactions, the event bus, URLs, Automations and redirection are already
          constructor dependencies. The remaining hidden dependencies are repository config/Sentry,
          query/collection helpers and the RedirectEvent contract. These can be wired locally while
          retaining live configuration reads and the repository shared with consumers; subscriber
          cleanup remains a separate lifecycle problem.
        preparation:
          - >-
            Inject repository config/Sentry and the remaining query, collection and redirect-event
            dependencies through the existing constructors; use the shared clock for Moment.
          - >-
            Keep redirection readiness explicit and preserve the same service/repository for Email
            and welcome-email callers when removing their initialization responsibility.
        reviewedAt: 44ad405bff4a8700af2ed51f833ebc08d25dddff
        sources:
          - core/server/services/link-tracking/index.js
          - core/server/services/link-tracking/link-click-repository.js
          - core/server/services/link-tracking/link-click-tracking-service.js
          - core/server/services/link-tracking/post-link-repository.js
          - core/server/services/member-welcome-emails/member-welcome-email-renderer.js
    entryPoints:
      - link-tracking/index.js
    notes: >-
      Preserve shared initialization, redirect dependencies, consumer-owned tracking gates and
      distinct newsletter/automation transaction paths. Subscription and cache lifetime still
      need explicit ownership before migration.
    contracts:
      exports:
        summary: >-
          CommonJS exports an eager wrapper with async receiver-dependent init and a
          LinkTrackingServiceWrapper constructor property used by tests. service and
          linkClickRepository publish only after implementation init resolves. Deep modules
          export constructible tracking/repository/data classes. Service methods return links,
          URLs or bulk-result objects; init resolves undefined and subscribe is also public.
          No readiness getter, reset or shutdown exists. Wrapper tests pin delayed service
          publication but not complete export shape, repository publication or detached calls.
        sources:
          - core/server/services/link-tracking/index.js
          - core/server/services/link-tracking/link-click-tracking-service.js
          - core/server/services/link-tracking/link-click-repository.js
          - core/server/services/link-tracking/post-link-repository.js
          - core/server/services/link-tracking/click-event.js
          - core/server/services/link-tracking/post-link.js
          - core/server/services/link-tracking/full-post-link.js
        tests:
          - test/unit/server/services/link-tracking/index.test.ts
          - test/unit/server/services/link-tracking/link-click-tracking-service.test.js
      construction:
        summary: >-
          The wrapper captures the shared automations API during construction and URL utilities
          at import. Default API import creates its database repository and imports shared
          request infrastructure; click processing itself calls its database method. Init
          requires initialized link redirection, builds post-link/click repositories and the
          implementation with borrowed models, redirect instances, DomainEvents and a transaction
          callback, then awaits subscription setup. Boot calls init; Member welcome emails asserts it
          is ready and borrows the instance. No separate production deep constructor was found. Root tests inject automation calls
          and model/transaction stubs, so successful wiring is asserted without real commit.
        sources:
          - core/server/services/link-tracking/index.js
          - core/server/services/link-tracking/link-click-tracking-service.js
          - core/server/services/automations/automations-api.ts
          - core/server/services/automations/database-automations-repository.ts
          - core/server/lib/request-external.js
          - core/server/services/member-welcome-emails/member-welcome-email-renderer.js
          - core/boot.js
        tests:
          - test/unit/server/services/link-tracking/index.test.ts
      consumers:
        summary: >-
          EmailRenderer retains the wrapper and reads service when rewriting newsletter links;
          welcome-email rendering retains the wrapper, awaits init for eligible links, and adds
          automation revision/run-step/member tracking. Admin links browse/edit read service per
          request. Redirect handling dispatches RedirectEvent without awaiting click processing;
          the subscriber persists raw clicks and optionally automation analytics. The emitted
          MemberLinkClickEvent feeds MembersEvents' last-seen updater. No runtime caller of the
          exposed click repository was found outside this graph. HTTP/renderer tests pin URL
          output and bulk edits; newsletter integration proves redirect/click presence but not
          the exact click row for each link. Welcome rendering tests replace this root's methods.
        sources:
          - core/server/services/email-service/email-service-wrapper.js
          - core/server/services/email-service/email-renderer.js
          - core/server/services/member-welcome-emails/member-welcome-email-renderer.js
          - core/server/api/endpoints/links.js
          - core/server/web/api/endpoints/admin/routes.js
          - core/frontend/web/routers/link-redirects.js
          - core/server/services/link-redirection/link-redirects-service.js
          - core/server/services/link-tracking/link-click-tracking-service.js
          - core/server/services/members-events/last-seen-at-updater.js
        tests:
          - test/e2e-api/admin/links.test.js
          - test/integration/services/email-service/batch-sending.test.js
          - test/unit/server/services/email-service/email-renderer.test.js
          - test/unit/server/services/member-welcome-emails/member-welcome-email-renderer.test.js
          - test/unit/server/services/members-events/last-seen-at-updater.test.js
      startup:
        summary: >-
          Reading an uninitialized redirection root rejects with its readiness error. Boot
          initializes redirection in its frontend-services phase before awaiting link tracking in the main
          service group and exposing the full app. Email composition earlier in that group
          retains the wrapper rather than reading its unpublished implementation. The welcome
          renderer also initializes on use, relying on the same prerequisite. Implementation
          init synchronously subscribes before its async promise resolves. Tests cover pending
          publication and one subscription for concurrent wrapper calls, not prerequisite errors,
          boot sequencing, early events or cleanup after partial subscription failure.
        sources:
          - core/boot.js
          - core/server/services/link-tracking/index.js
          - core/server/services/link-tracking/link-click-tracking-service.js
          - core/server/services/link-redirection/index.ts
          - core/server/services/email-service/email-service-wrapper.js
          - core/server/services/member-welcome-emails/member-welcome-email-renderer.js
        tests:
          - test/unit/server/services/link-tracking/index.test.ts
          - test/integration/services/email-service/batch-sending.test.js
      lifetime:
        summary: >-
          Concurrent wrapper calls await one internal initialization; rejection clears its
          promise for retry, while success retains graph and fulfilled promise. Implementation
          init guards its subscription, but calling public subscribe again adds another listener.
          Clearing a successful wrapper's service alone cannot recreate it; event-bus resets or
          wrapper replacement can leave missing or old listeners. Click repository memoization
          retains member-lookup promises by UUID only, including null; no size/TTL/invalidation
          exists and transaction options are not part of the key. Rejections evict their entry.
          Tests pin concurrency, retry, memoized reuse and failure eviction, not cache staleness,
          cross-transaction visibility, bus reset, retained collaborators or repeated boot.
        sources:
          - core/server/services/link-tracking/index.js
          - core/server/services/link-tracking/link-click-tracking-service.js
          - core/server/services/link-tracking/link-click-repository.js
        tests:
          - test/unit/server/services/link-tracking/index.test.ts
          - test/unit/server/services/link-tracking/link-click-tracking-service.test.js
          - test/unit/server/services/link-tracking/link-click-repository.test.js
      configuration:
        summary: >-
          Click-cache and missing-member reporting flags are config accessor reads per save,
          both false by default; disabling cache use does not clear old entries. URL utilities
          and redirect/API collaborators stay captured. Newsletter sending snapshots the click
          setting onto its email; automation polling derives recipient tracking from
          email_track_clicks. The root's methods and redirect subscriber do not enforce a
          global tracking switch. Automation recipient analytics additionally requires its saved
          track_clicks value. Tests pin caller gating, fixed config modes, preserved URL/ref/hash
          and NQL escaping; test config replacement establishes no production hot reload.
        sources:
          - core/server/services/link-tracking/link-click-repository.js
          - core/server/services/link-tracking/link-click-tracking-service.js
          - core/shared/config/defaults.json
          - core/shared/url-utils.ts
          - core/server/services/email-service/email-service.js
          - core/server/services/email-service/batch-sending-service.js
          - core/server/services/automations/poll.ts
          - core/server/services/automations/database-automations-repository.ts
        tests:
          - test/unit/server/services/link-tracking/link-click-repository.test.js
          - test/unit/server/services/link-tracking/link-click-tracking-service.test.js
          - test/unit/server/services/member-welcome-emails/member-welcome-email-renderer.test.js
          - test/integration/services/email-service/batch-sending.test.js
          - test/unit/server/services/automations/database-automations-repository.test.ts
      resources:
        summary: >-
          Owns a RedirectEvent subscription, memoization cache and untracked async work; owns no
          database pool, timer or HTTP agent. Shared redirection owns its cache/listener, the
          default automations module imports shared request agents, and MembersEvents owns
          last-seen effects. No disposer or production drain is registered. Newsletter URL
          creation inserts a redirect then links its post separately; clicks without complete
          automation identity use a standalone insert. Automation clicks share one transaction
          with recipient/count updates, scheduling MemberLinkClickEvent only after executionPromise
          fulfillment. Tests enforce callback/options wiring and simulated commit/rollback delivery;
          a single-connection SQLite test proves lookup connection reuse, while automation SQL
          tests prove analytics rollback separately from raw click persistence.
        sources:
          - core/server/services/link-tracking/index.js
          - core/server/services/link-tracking/link-click-tracking-service.js
          - core/server/services/link-tracking/link-click-repository.js
          - core/server/services/link-tracking/post-link-repository.js
          - core/server/services/link-redirection/link-redirect-repository.js
          - core/server/services/automations/automations-api.ts
          - core/server/services/automations/database-automations-repository.ts
          - core/server/lib/request-external.js
          - core/server/services/members-events/last-seen-at-updater.js
          - core/server/ghost-server.ts
          - package.json
        tests:
          - test/unit/server/services/link-tracking/index.test.ts
          - test/unit/server/services/link-tracking/link-click-repository.test.js
          - test/unit/server/services/link-tracking/link-click-tracking-service.test.js
          - test/unit/server/services/automations/database-automations-repository.test.ts
      failures:
        summary: >-
          Missing members skip insert/analytics, optionally reporting to Sentry. Lookup/insert
          and direct subscriber errors reject; DomainEvents catches/logs handler failures so
          HTTP redirects do not wait for or report failed click work. Post-commit synchronous
          dispatch errors are captured; a rejected executionPromise suppresses delivery, while nontransactional
          dispatch throws after insertion. URL creation failures can leave an unlinked redirect;
          email LinkReplacer catches replacement errors and returns original HTML without undoing
          writes. Invalid old-link filters become BadRequestError; other URL/query failures reject,
          while bulk model errors can become unsuccessful result data. Units pin selected errors
          and simulated delivery; complete consumer/transaction failure boundaries remain open.
        sources:
          - core/server/services/link-tracking/index.js
          - core/server/services/link-tracking/link-click-tracking-service.js
          - core/server/services/link-tracking/link-click-repository.js
          - core/server/services/link-tracking/post-link-repository.js
          - core/server/services/link-redirection/link-redirects-service.js
          - core/server/lib/link-replacer.js
          - core/server/models/base/plugins/bulk-operations.js
          - package.json
        tests:
          - test/unit/server/services/link-tracking/index.test.ts
          - test/unit/server/services/link-tracking/link-click-repository.test.js
          - test/unit/server/services/link-tracking/link-click-tracking-service.test.js
          - test/unit/server/services/link-tracking/post-link-repository.test.js
    unverified: []
    blockers:
      - >-
        Pin root shape, repository publication, missing redirect prerequisite, successful repeat
        init and retained dependency identity. Define ownership for subscription disposal, bus
        reset, partial initialization and pending clicks during shutdown; fulfilled init promises
        and implementation guards currently survive independently of event-bus listeners.
      - >-
        Characterize UUID cache size/lifetime, cached null/deleted/changed members, config toggles
        and lookup reuse across transactions. Default-disabled caching still ignores transaction
        options when enabled; current tests reuse the same transaction and do not establish isolation.
      - >-
        Exercise the real automation redirect through raw click insertion, recipient analytics,
        commit and emitted last-seen work, including rollback and duplicate clicks. Explicit/manual
        rollback can fulfill executionPromise under Knex defaults; cover that separately from
        transaction callback rejection. Existing fake
        transaction tests prove arguments/rejection and repository SQLite tests prove analytics
        alone. Strengthen newsletter click assertions with actual member/redirect filters and
        row identity; current getAll calls pass ignored member_id/link_id options.
      - >-
        Pin partial URL-creation writes, renderer fallback, bulk error results, click counts and
        edit-time ordering/reset behavior through real consumers. PostLinkRepository's sole unit
        checks result shape; Admin browse uses broad ID/URL/count matchers. Preserve current
        caller-owned tracking gates, URL encoding and public redirect continuation on click failure.
      - >-
        Record data-object quirks before migration: ClickEvent without id overwrites its generated
        event_id with undefined, and repository getAll reconstructs no saved event ID/timestamp.
        save replaces event_id after persistence; no constructor/getAll assertion pins these boundaries.
  machine-payments:
    kind: directory
    disposition: migrate
    status: audited
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: complex
      modelImpact: fits
      reason: >-
        Unify boot and request-owned initialization, publish after adapter setup and
        best-effort address prewarming, and decide the change from permanently cached
        startup rejection to explicit retry. Keep pre-init false guards and live enablement
        deliberate. One retained service already supports disabled operation; per-rail
        availability, lazy SDKs and provider caches can stay inside it.
      reviewedAt: f276513ba32b7114fd5cc4f7b02d3f82c0c31e3e
      sources:
        - core/server/services/machine-payments/index.js
        - core/server/services/machine-payments/service.ts
        - core/server/services/machine-payments/adapters/x402-adapter.ts
        - core/frontend/web/site.js
        - test/integration/services/machine-payments/orchestration.test.js
      dependencies:
        complexity: complex
        reason: >-
          The root composes a graph, but implementations still supply hidden defaults for models,
          settings, labs and Stripe clients. MPP and x402 load SDKs lazily, and Stripe factories
          observe credential changes. Moving imports alone would change request-time loading and
          live configuration or leave alternative composition paths behind.
        preparation:
          - >-
            Make production dependencies explicit across the orchestrator, content loader, deposit
            store and payment recorder; inject model/currency lookup and live settings providers
            from the root.
          - >-
            Supply root-owned lazy SDK loaders through the existing x402 runtime/config and MPP
            factory seams, retaining first-use loading and credential-keyed client refresh.
          - >-
            Inject logging and shared eligibility functions without capturing dynamic enablement as
            a boot-time boolean.
        reviewedAt: 44ad405bff4a8700af2ed51f833ebc08d25dddff
        sources:
          - core/server/services/machine-payments/index.js
          - core/server/services/machine-payments/service.ts
          - core/server/services/machine-payments/content-loader.ts
          - core/server/services/machine-payments/adapters/mpp-adapter.ts
          - core/server/services/machine-payments/adapters/x402-adapter.ts
          - core/server/services/machine-payments/stripe/deposit-address-store.ts
          - core/server/services/machine-payments/stripe/payment-recorder.ts
          - core/server/services/machine-payments/README.md
    entryPoints:
      - machine-payments/index.js
    notes: >-
      Preserve the boot/request initialization boundary, per-rail availability and one-request
      content unlock. Root lifecycle, credential rotation, provider resources and failures after
      settlement remain migration blockers despite substantial deep implementation coverage.
    contracts:
      exports:
        summary: >-
          CommonJS exports an eager wrapper instance with public service initially null, init,
          isEnabled, isPurchasable and async challengeOrFulfill, plus the wrapper constructor.
          Methods require their receiver. init resolves the implementation; availability methods
          return false until initialization completes, while challengeOrFulfill awaits init itself.
          The deep class exposes synchronous eligibility; getTerms resolves payment terms and
          challengeOrFulfill resolves a Fetch Response. Adapters expose canHandle/challenge/fulfill.
          Deep tests assert
          402 challenges, 200 paid markdown with private/no-store, 403 rejection and 503 unavailable
          responses; they do not pin root export shape, receiver or publication contracts.
        sources:
          - core/server/services/machine-payments/index.js
          - core/server/services/machine-payments/service.ts
          - core/server/services/machine-payments/types.ts
        tests:
          - test/unit/server/services/machine-payments/service.test.ts
          - test/unit/server/services/machine-payments/adapters.test.ts
      construction:
        summary: >-
          First init creates a deposit-address store, payment recorder, event repository retaining
          models.MachinePaymentEvent, content loader retaining the URL facade, pricing, MPP and
          x402 adapters. It awaits x402 config validation, includes that adapter only when ready,
          then publishes MachinePaymentsService with settings/labs facades and a Stripe-status
          callback. Creation occurs even when machine payments is disabled. ContentLoader and
          the store lazily retain Post/Settings models. No other production deep construction was
          found; tests construct these classes directly with fake providers and, separately, real
          database models, so they do not establish the wrapper's composition or readiness.
        sources:
          - core/server/services/machine-payments/index.js
          - core/server/services/machine-payments/content-loader.ts
          - core/server/services/machine-payments/stripe/deposit-address-store.ts
          - core/server/services/machine-payments/events/machine-payment-event-repository.ts
        tests:
          - test/integration/services/machine-payments/content-loader.test.js
          - test/integration/services/machine-payments/orchestration.test.js
      consumers:
        summary: >-
          Boot initializes the root; each site app retains it in machinePaymentsService. The
          entry markdown controller obtains that root from the app, checks isPurchasable and
          supplies render callbacks to challengeOrFulfill only on explicit .md paid/tier requests.
          It copies Fetch status/headers/body to Express; already-authorized members never receive
          their full body as an unpaid preview. HTML ghost_head uses shared eligibility directly,
          not the initialized root, for markdown alternate links. LLMS discovery uses its own
          public-preview path, not this service. Frontend HTTP tests cover delegation, rendered
          markdown/header copying, previews and HTML credential isolation but stub fulfillment;
          controller units cover the authorized-member refusal with a fake root.
        sources:
          - core/boot.js
          - core/frontend/web/site.js
          - core/frontend/services/routing/controllers/entry/markdown.ts
          - core/frontend/services/routing/controllers/entry.ts
          - core/frontend/helpers/ghost_head.js
          - core/shared/machine-payments.ts
          - core/frontend/services/llms/service.js
        tests:
          - test/e2e-frontend/machine-payments-markdown.test.js
          - test/unit/frontend/services/routing/controllers/entry.test.ts
          - test/e2e-frontend/llms-routes.test.js
      startup:
        summary: >-
          Boot awaits init in its service group after Stripe initialization. x402 init validates
          and captures config without loading its runtime. Root service publication precedes
          enabled-only prewarming of both configured network addresses, even if x402 was excluded;
          Promise.allSettled waits for both and logs rejections before opening availability gates.
          Disabled boot still constructs the graph, then request-time enablement can use it without
          prewarming. MPP loads mppx on request; x402 loads its runtime on first dispatch, which can
          be challenge or fulfill. Frontend HTTP setup boots Ghost, but tests do not isolate
          concurrent root init, prewarm completion/failure, partial publication or request-init races.
        sources:
          - core/boot.js
          - core/server/services/machine-payments/index.js
          - core/server/services/machine-payments/adapters/mpp-adapter.ts
          - core/server/services/machine-payments/adapters/x402-adapter.ts
        tests:
          - test/unit/server/services/machine-payments/adapters.test.ts
          - test/e2e-frontend/machine-payments-markdown.test.js
      lifetime:
        summary: >-
          Concurrent root init calls share one stored promise; success retains one graph and later
          init resolves it, while a rejected promise prevents retry. No reset/stop/dispose exists.
          The store retains addresses by network and deduplicates only same-instance in-flight
          creation. Store/recorder clients refresh on secret-key change when needed; MPP builds
          a Stripe client each run but its retained Mppx cache key omits that client's credentials.
          MPP keeps one replay store across runtime rebuilds. x402 attempts configuration once,
          retains runtime/facilitator/scheme and bounds route apps at 128; its cache key includes
          route, recipient, price, network and challenge/fulfill mode, not description/mimeType.
          Tests assert same-network deduplication, Mppx reuse, lazy x402 loading and LRU eviction
          with fake runtimes, not root restart, rotation, cross-process races or cleanup.
        sources:
          - core/server/services/machine-payments/index.js
          - core/server/services/machine-payments/adapters/mpp-adapter.ts
          - core/server/services/machine-payments/adapters/x402-adapter.ts
          - core/server/services/machine-payments/stripe/deposit-address-store.ts
          - core/server/services/machine-payments/stripe/payment-recorder.ts
        tests:
          - test/unit/server/services/machine-payments/adapters.test.ts
          - test/unit/server/services/machine-payments/deposit-address-store.test.ts
      configuration:
        summary: >-
          Eligibility reads labs, machine_payments_enabled === true, llms_enabled !== false and
          Stripe connectivity each call. Pricing reads current settings, validates positive safe
          integer minor units, and uses configured currency, first active paid tier currency, then
          USD; default amount is 100. SPT uses configured fiat; Tempo maps the same minor-unit
          amount to USDC without FX, while x402 accepts USD only. MPP reads current profile/secret
          settings and config getters
          per run; x402 captures a feature-local parsed config once, defaults to enabled Base
          mainnet/base/HTTPS facilitator, and rejects unsupported networks and the known mainnet
          pairing with the x402.org testnet facilitator.
          Stripe preview clients read endpoint overrides on construction. Config's validated tree
          is immutable; test stubs/set/reset do not imply production reload. Deposit addresses
          persist as a network map with legacy strings treated as Tempo-only. Deep tests cover
          selected gates, configured pricing, adapter config and endpoint options; Admin HTTP
          tests reject amount 0 and two-letter currency, not every default/setting transition.
        sources:
          - core/shared/machine-payments.ts
          - core/shared/config/SCHEMA.md
          - core/shared/config/defaults.json
          - core/server/services/machine-payments/pricing.ts
          - core/server/services/machine-payments/service.ts
          - core/server/services/machine-payments/adapters/mpp-adapter.ts
          - core/server/services/machine-payments/adapters/x402-adapter.ts
          - core/server/services/machine-payments/stripe/stripe-client-options.ts
          - core/server/services/machine-payments/stripe/deposit-address-store.ts
          - core/server/models/settings.js
          - core/server/data/schema/default-settings/default-settings.json
          - core/server/api/endpoints/utils/serializers/input/settings.js
        tests:
          - test/unit/server/services/machine-payments/service.test.ts
          - test/unit/server/services/machine-payments/eligibility.test.ts
          - test/unit/server/services/machine-payments/adapters.test.ts
          - test/unit/server/services/machine-payments/stripe-client-options.test.js
          - test/unit/server/services/machine-payments/deposit-address-store.test.ts
          - test/integration/services/machine-payments/content-loader.test.js
          - test/integration/services/machine-payments/orchestration.test.js
          - test/e2e-api/admin/settings.test.js
      resources:
        summary: >-
          The graph borrows database models, settings/labs and URL facades; owns address/in-flight
          maps, provider clients, MPP replay memory and x402 route apps. Requests can mint/persist
          addresses, call payment providers, load full published content, persist events and record
          crypto PaymentIntents; no member session or tier is granted. Installed Stripe uses
          shared keep-alive agents and default request timeouts; x402's per-app middleware starts
          facilitator discovery, with SDK fetch deadlines/retry timers; MPP's in-memory replay
          map has no expiry/size bound and Tempo uses RPC clients. Ghost supplies no cancellation,
          drain or cleanup registration for these resources. Event rows are append-only through
          the model, unique by protocol/reference, cascade with posts and enter optional backups.
          Real-DB tests prove sequential duplicate reuse and different-protocol separation, not
          concurrent unique-conflict recovery, SDK resources, restart replay or shutdown.
        sources:
          - core/server/services/machine-payments/index.js
          - core/server/services/machine-payments/content-loader.ts
          - core/server/services/machine-payments/adapters/mpp-adapter.ts
          - core/server/services/machine-payments/adapters/x402-adapter.ts
          - core/server/services/machine-payments/stripe/deposit-address-store.ts
          - core/server/services/machine-payments/stripe/payment-recorder.ts
          - core/server/services/machine-payments/events/machine-payment-event-repository.ts
          - core/server/models/machine-payment-event.js
          - core/server/data/schema/schema.js
          - core/server/data/exporter/table-lists.js
          - core/boot.js
        tests:
          - test/unit/server/services/machine-payments/deposit-address-store.test.ts
          - test/unit/server/services/machine-payments/adapters.test.ts
          - test/integration/services/machine-payments/content-loader.test.js
          - test/integration/services/machine-payments/orchestration.test.js
      failures:
        summary: >-
          Raw eligibility and full-content deliverability precede settlement; full load precedes
          fulfill despite older comments saying otherwise. First matching adapter handles credentials.
          Fulfill errors become 403 for statusCode 403 or logged 503 otherwise. Ledger save follows
          settlement: failure logs/returns 503; duplicate returns 403 before recorder/rendering.
          Recording errors log and still serve content; ledger rows are not updated with later
          recorder ids. Challenges run allSettled and use any returned response, else 503; x402
          logs/returns null on challenge failure, and MPP can retain SPT after address failure.
          Pricing, raw/full content lookup, canHandle and render exceptions propagate. No rollback
          joins provider settlement, database ledger and response delivery. Source also caches a
          new address before persistence and marks x402 runtime loaded before constructing its
          facilitator/scheme. Store/recorder update their key marker before constructing a replacement
          Stripe client; a factory throw can leave the old client associated with the new marker.
          These are untested source-only failure edges. Tests assert null-content rejection, replay
          refusal, invocation order,
          unavailable challenge and selected adapter failures, not these partial-failure channels.
        sources:
          - core/server/services/machine-payments/service.ts
          - core/server/services/machine-payments/content-loader.ts
          - core/server/services/machine-payments/adapters/mpp-adapter.ts
          - core/server/services/machine-payments/adapters/x402-adapter.ts
          - core/server/services/machine-payments/stripe/deposit-address-store.ts
          - core/server/services/machine-payments/events/machine-payment-event-repository.ts
        tests:
          - test/unit/server/services/machine-payments/service.test.ts
          - test/unit/server/services/machine-payments/adapters.test.ts
          - test/unit/server/services/machine-payments/content-loader.test.ts
          - test/integration/services/machine-payments/orchestration.test.js
    unverified: []
    blockers:
      - Pin root export/receiver behavior, concurrent boot/request init, prewarm gating, disabled boot then enablement, partial publication and retained rejection before changing initialization.
      - Resolve and test MPP retaining an old Stripe client when only active credentials change; characterize failed store/recorder client replacement, address reuse across account changes and captured x402 config versus live settings.
      - Resolve and test address caching before failed persistence, two-network or multi-instance persistence races, provider failure retry and x402 partial runtime construction.
      - Enforce awaited load/settle/ledger/record sequencing with deferred failures; current callOrder assertions prove invocation order, not completion barriers. Cover charged-but-no-content, ledger failure, recorder continuation, render failure and durable replay across restart/concurrent requests.
      - Define ownership and bounds for MPP replay memory, cached apps, SDK requests/retry timers and shared agents, including cancellation/drain on shutdown and graph replacement.
      - Add a coherent real-root HTTP/provider-fake/real-database contract for paid posts and pages, mixed tiers, signed-in previews, pricing/default currency, both-credential precedence and provider failures; existing HTTP tests stub fulfillment and deep orchestration tests stub persistence/providers.
  media-inliner:
    kind: directory
    disposition: migrate
    migrated: true
    status: audited
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: straightforward
      modelImpact: fits
      reason: >-
        The boot-owned default root returns one ready ExternalMediaInliner once the storage
        adapters and models exist; boot registers the job handler with that instance and
        content import reads it per run. Repeated init now retains the instance, which no
        production path relied on changing; a second boot in one process no longer
        re-resolves the adapters, so a storage adapter broken between boots fails at its first
        selection instead of at boot. Storage selection reads the uploads policy and looks up
        the active adapter on every call; init still resolves all three so a broken adapter
        fails the first boot.
      reviewedAt: 940a713d06d08ca9294bb767e8419cbd58f7b13e
      sources:
        - core/server/services/media-inliner/index.ts
        - core/server/services/media-inliner/external-media-inliner.js
        - core/server/services/jobs-service/register-job-handlers.ts
        - core/boot.js
        - test/unit/server/services/media-inliner/root.test.ts
      dependencies:
        complexity: straightforward
        reason: >-
          The root composes models, the three storage adapters, live upload configuration,
          logging, the external request client, URL utilities and lazy file-type and HEIC
          loaders in index.ts; the implementation receives them through its constructor and
          keeps first-use loading and the fallback on a failed file-type load. mime-types, path,
          node:vm and the shared image-content and svg-sanitizer helpers are stateless and stay
          imported directly under exact allowances; no disallowed dependency remains.
        preparation: []
        reviewedAt: b7cad8406041d09d9e4085c5f981f958d454fc76
        sources:
          - core/server/services/media-inliner/index.ts
          - core/server/services/media-inliner/external-media-inliner.js
          - core/server/services/jobs-service/register-job-handlers.ts
          - core/boot.js
    entryPoints:
      - media-inliner/index.ts
    notes: >-
      Preserve the boot-owned inliner, retained job handlers and per-import media consumers.
      Shared transport/storage ownership needs contracts before any disposal is added;
      successful job completion does not mean every resource was inlined.
    contracts:
      exports:
        summary: >-
          The TypeScript root default-exports a defineService initializer with init and a
          service getter that throws IncorrectUsageError until the inliner is published; there
          is no shutdown/reset and no extras. The deep module exports ExternalMediaInliner,
          including receiver-dependent importUrl and inline plus processing helpers and static
          findMatches. importUrl resolves stored/failed result objects; inline resolves undefined.
          Unit tests pin injected importUrl stages and helpers. Root units pin the getter throwing
          before init, init resolving undefined and the getter then returning the same ready
          inliner synchronously.
        sources:
          - core/server/services/media-inliner/index.ts
          - core/server/services/media-inliner/external-media-inliner.js
          - core/server/services/media-inliner/types.ts
        tests:
          - test/unit/server/services/media-inliner/test/external-media-inliner.test.js
          - test/unit/server/services/media-inliner/root.test.ts
      construction:
        summary: >-
          Importing the root creates no inliner. The first init loads the class, models and
          collaborators, obtains the media/image/file adapters and creates one inliner with
          retained models, logging, the external request client, URL utilities, lazy codec
          loaders and a storage callback; later init calls return the same instance. The factory
          is synchronous, so the kernel publishes once its promise settles. Construction only
          stores references, but requiring the request client creates shared HTTP/HTTPS agents.
          Root tests stub adapter lookup and exercise the resulting selector, and pin that import
          resolves no adapter, that init resolves the three storage adapters once and publishes
          one inliner, that an adapter lookup failure publishes nothing and allows an explicit
          retry, and that the root's loaders convert a HEIC download.
        sources:
          - core/server/services/media-inliner/index.ts
          - core/server/services/media-inliner/external-media-inliner.js
          - core/server/lib/request-external.js
        tests:
          - test/unit/server/services/media-inliner/service.test.ts
          - test/unit/server/services/media-inliner/root.test.ts
      consumers:
        summary: >-
          Boot passes the ready service to a retained external-media job handler. Admin
          db.inlineMedia dispatches explicit/default domains and returns after enqueue, not
          completion. Content import instead reads the service getter when creating each run's
          PostMediaInliner, which retains that instance and caches URL promises across its rows.
          Handler/endpoint units use stubs; a real job integration proves a stored feature-image
          URL and completion log. CSV HTTP tests cover real referenced-media composition,
          reuse, skipped local URLs and unsupported-row isolation, with network interception.
          Root units register the real handler with the published inliner and show it keeps
          that inliner across a later init, and that a PostMediaInliner over the published
          inliner caches per run while a later run downloads the same URL again.
        sources:
          - core/boot.js
          - core/server/services/jobs-service/register-job-handlers.ts
          - core/server/services/jobs-service/jobs-service.ts
          - core/server/api/endpoints/db.js
          - core/server/services/content-import/index.ts
          - core/server/services/content-import/import/importer.ts
          - core/server/services/content-import/import/media.ts
        tests:
          - test/unit/server/services/jobs-service/register-job-handlers.test.ts
          - test/unit/api/endpoints/db.test.js
          - test/integration/services/media-inliner/external-media-inliner-job.test.ts
          - test/e2e-api/admin/posts-importer.test.js
          - test/unit/server/services/content-import/import/media.test.ts
          - test/unit/server/services/media-inliner/root.test.ts
      startup:
        summary: >-
          Required boot initialization awaits media init in the service Promise.all before
          reading the service, registering job handlers and starting job delivery. Rejection
          prevents those later steps but does not undo other services' concurrent initialization.
          The service getter has only the kernel's publication guard; the class accepts
          incomplete injected dependencies. Boot units pin that pending media init blocks handler
          registration and job start and that its rejection propagates without starting them;
          root units pin adapter lookup failure publishing nothing and a later init succeeding.
          Construction acquires nothing, so there is no partial-startup cleanup to pin.
        sources:
          - core/boot.js
          - core/server/services/media-inliner/index.ts
          - core/server/services/media-inliner/external-media-inliner.js
          - core/server/services/jobs-service/register-job-handlers.ts
        tests:
          - test/integration/services/media-inliner/external-media-inliner-job.test.ts
          - test/e2e-api/admin/posts-importer.test.js
          - test/unit/boot/init-services.test.ts
          - test/unit/server/services/media-inliner/root.test.ts
      lifetime:
        summary: >-
          The kernel retains the first published inliner across later init calls, so handlers,
          running imports and later reads share one instance; a failed first init publishes
          nothing and a later explicit init runs the factory again. The inliner has no shared URL
          cache or concurrency guard; inline downloads duplicate occurrences independently.
          Content import owns a separate cache per run. Each inliner retains the file-type
          function after its first download; the deep module keeps a reusable VM context whose
          match inputs are cleared in finally. Root units pin retention across repeated init, a
          registered handler and an import run keeping the inliner, the published inliner
          surviving a later failed init, and no root-wide URL cache: a second import run downloads
          a URL the first run already stored. Seam units pin that every occurrence in one body
          downloads again. Restart remains unasserted.
        sources:
          - core/server/services/media-inliner/index.ts
          - core/server/services/media-inliner/external-media-inliner.js
          - core/server/services/jobs-service/register-job-handlers.ts
          - core/server/services/content-import/index.ts
          - core/server/services/content-import/import/media.ts
        tests:
          - test/unit/server/services/media-inliner/root.test.ts
          - test/unit/server/services/media-inliner/external-media-inliner-seams.test.ts
      configuration:
        summary: >-
          Init resolves the three storage adapters so a broken one fails boot; the root's
          selector then reads allowed upload extensions through config.get and looks up the
          active adapter through the adapter manager on each selection. Images take precedence and need allowed contents,
          with SVG sanitization before selection; media/files use extension membership. Network
          agent options are captured when request-external loads, while its environment/site-URL
          guards read the accessor per request. These are immutable-config access/capture rules,
          not production hot reload. Root units cover storage choices and pin that a retained
          inliner reads a replaced uploads policy and returns the replaced adapters on each
          selection; class units cover SVG, conversions, URL formatting, CDN paths and
          regex matching. Body patterns are regexes with bounded VM execution; simple fields use
          literal startsWith(domain). Seam units pin that distinction for one pattern domain and
          that the sniffed file type wins over Content-Type and the URL.
        sources:
          - core/server/services/media-inliner/index.ts
          - core/server/services/media-inliner/external-media-inliner.js
          - core/server/lib/image/image-content.ts
          - core/server/lib/image/svg-sanitizer.ts
          - core/server/lib/request-external.js
          - core/shared/config/validated.ts
        tests:
          - test/unit/server/services/media-inliner/service.test.ts
          - test/unit/server/services/media-inliner/test/external-media-inliner.test.js
          - test/unit/server/services/media-inliner/root.test.ts
          - test/unit/server/services/media-inliner/external-media-inliner-seams.test.ts
      resources:
        summary: >-
          The inliner borrows models, storage and the shared request client the root injects. It
          buffers each download, may load the HEIC codec through the root's loader and writes
          files before updating models, without a transaction or rollback linking those effects. It owns no timer/listener, pending-request registry
          or cancellation/drain hook. Jobs owns queued work and delegates shutdown to its backend;
          no media-specific abort is passed to downloads. Request defaults include redirects and
          a 10-second timeout; test mode changes timeout/retries. Existing intercepted requests,
          fake storage units and real stored-URL integration do not exercise pending shutdown,
          production retry policy, socket disposal or orphan-file cleanup.
        sources:
          - core/server/services/media-inliner/external-media-inliner.js
          - core/server/services/media-inliner/index.ts
          - core/server/lib/request-external.js
          - core/server/services/jobs-service/jobs-service.ts
          - core/boot.js
        tests:
          - test/integration/services/media-inliner/external-media-inliner-job.test.ts
          - test/unit/server/services/media-inliner/test/external-media-inliner.test.js
      failures:
        summary: >-
          Download errors normally log and become null; importUrl maps download, extraction and
          storage exceptions into typed failures. Selector/content-check errors and invalid SVGs
          return null and therefore become unsupported. HEIC conversion errors log and retain the
          original body/type; missing codec loading escapes as extraction failure. Legacy inlining
          rethrows result.error, including falsy values, to per-resource catches that log and
          continue. Query-list failures escape inline; previous writes remain. Regex timeouts skip
          that pattern for later post bodies, while other patterns continue. Units pin injected
          importUrl stage mappings, selector/SVG rejection, selected error logs and timeout
          skipping. Seam units pin continuation after a failed post or tag write, an expected
          download failure leaving that post untouched while later posts proceed, query failures
          rejecting inline before any write or after earlier writes, the HEIC codec required on
          first conversion only, a codec that cannot load escaping as an extraction failure, and
          a conversion failure keeping the original bytes and extension, and a failed file-type
          load falling back to Content-Type and retried on the next download. Handler units pin
          rejection forwarding. CSV
          tests prove row isolation for unsupported files and no post after an injected
          unexpected throw, not rollback of already-stored remote media or every error stage.
        sources:
          - core/server/services/media-inliner/external-media-inliner.js
          - core/server/services/media-inliner/index.ts
          - core/server/services/jobs-service/register-job-handlers.ts
          - core/server/services/content-import/import/media.ts
          - core/server/services/content-import/import/importer.ts
        tests:
          - test/unit/server/services/media-inliner/test/external-media-inliner.test.js
          - test/unit/server/services/media-inliner/external-media-inliner-seams.test.ts
          - test/unit/server/services/jobs-service/register-job-handlers.test.ts
          - test/e2e-api/admin/posts-importer.test.js
    unverified: []
    blockers:
      - >-
        Pending validation/downloads across a configuration change remain unpinned; the
        selection policy and the active adapter are read per call.
      - >-
        Overlapping jobs/imports, atomic writes, cleanup of stored files after a failed model
        write and exactly-once downloads remain unestablished. Per-resource best-effort
        continuation and query failures escaping inline are now explicit.
      - >-
        Establish shared request/storage ownership and pending-work shutdown before adding disposal.
        Production timeout/retry/redirect behavior differs from the test harness; cancellation
        and orphan cleanup cannot be inferred from a bounded request or successful queue drain.
  member-attribution:
    kind: directory
    disposition: migrate
    status: audited
    migrated: true
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: straightforward
      modelImpact: fits
      reason: >-
        One composition-only graph fits the helper. Preserve its attribution service,
        builder and outbound tagger, and explicitly await initialization before Staff,
        Members and Email capture them; the current Promise.all argument order relies on
        synchronous publication. Live settings callbacks and per-call attribution data do
        not require a lifecycle extension.
      reviewedAt: f276513ba32b7114fd5cc4f7b02d3f82c0c31e3e
      sources:
        - core/server/services/member-attribution/index.ts
        - core/server/services/staff/index.ts
        - core/server/services/email-service/email-service-wrapper.js
        - core/boot.js
        - test/e2e-server/services/member-attribution.test.js
      dependencies:
        complexity: straightforward
        reason: >-
          The root already wires models, URLs, live tracking settings and the translator/builder
          graph. Remaining implementation imports are a ReferrerParser constructor, slugify,
          LinkReplacer and toPlain, all with bounded constructor/function seams. Staff, Members and
          Email capture this graph during boot, so explicit readiness must replace reliance on
          synchronous publication inside Promise.all arguments.
        preparation:
          - >-
            Construct the referrer parser in index.ts and pass it plus the outbound-link and
            plain-data helpers into the existing collaborators.
          - >-
            Keep site/tracking getter timing unchanged and await attribution before Staff, Members
            and Email capture its service, builder or tagger.
        reviewedAt: 44ad405bff4a8700af2ed51f833ebc08d25dddff
        sources:
          - core/server/services/member-attribution/index.ts
          - core/server/services/member-attribution/referrer-translator.js
          - core/server/services/member-attribution/outbound-link-tagger.js
          - core/server/services/member-attribution/url-translator.js
          - core/server/services/staff/index.ts
          - core/server/services/members/api.js
          - core/boot.js
    entryPoints:
      - member-attribution/index.ts
    notes: >-
      Preserve the three exposed collaborators, retained consumers and different configuration
      timing. Attribution computes and reads data; its consumers own event persistence, mail,
      subscriptions and HTTP handling. Tests-only root and retained-consumer coverage was
      refreshed against e7ab0fe5a8406036cfb0d790e8db7afdb58bfe36; the historical audit and
      migration assessment revisions remain above. Initializer adoption was inspected against
      8de2cd47ce1614f877695695a48b41e69f26e650. The historical helper-injection preparation
      above is superseded by source-reviewed exact direct-library allowances included
      with this migration.
    contracts:
      exports:
        summary: >-
          The TypeScript root default-exports defineService with awaited init and a guarded
          service getter. Its ready MemberAttributionService instance exposes the existing
          attributionBuilder and the constructor-wired outboundLinkTagger alongside normal methods.
          CommonJS consumers read the default export. No transitional extras are exposed.
          Deep modules export constructible service/builder/translator/tagger/history classes;
          Attribution itself is produced by builder.build/getAttribution. Service methods mix
          synchronous URL/event mapping with async history/context/resource queries. Returned
          Attribution objects expose fields, getResource and async fetchResource. Tagging returns
          copied URLs or async HTML. There is no reset or shutdown hook. Existing root tests
          mechanically adopt guarded pre-init access and awaited initialization while retaining
          collaborator identity and operation assertions. Deep-class behavior tests are unchanged.
        sources:
          - core/server/services/member-attribution/index.ts
          - core/server/services/member-attribution/member-attribution-service.js
          - core/server/services/member-attribution/attribution-builder.js
          - core/server/services/member-attribution/outbound-link-tagger.js
          - core/server/services/member-attribution/url-history.js
        tests:
          - test/unit/server/services/member-attribution/index.test.ts
          - test/unit/server/services/member-attribution/service.test.ts
          - test/unit/server/services/member-attribution/attribution.test.ts
          - test/unit/server/services/member-attribution/outbound-link-tagger.test.ts
      construction:
        summary: >-
          Import retains shared URL service/utilities, settings and config. Boot is the only
          production init caller found. init creates URL and referrer translators, builder,
          tagger and service with borrowed model classes and callbacks. ReferrerTranslator
          constructs its own parser with site/admin URLs. Constructors perform no database
          query or subscription. The factory returns the complete service graph for atomic
          publication; a failed attempt exposes no partial collaborators. Root tests preserve
          retry after URL configuration and late event-model acquisition failures. Allocated
          helpers own no external resource requiring failure cleanup.
        sources:
          - core/server/services/member-attribution/index.ts
          - core/server/services/member-attribution/url-translator.js
          - core/server/services/member-attribution/referrer-translator.js
          - core/server/services/member-attribution/attribution-builder.js
          - core/server/services/member-attribution/outbound-link-tagger.js
          - core/server/services/member-attribution/member-attribution-service.js
          - core/boot.js
        tests:
          - test/unit/server/services/member-attribution/index.test.ts
      consumers:
        summary: >-
          Members API construction retains service in its router controller, BREAD service and
          event repository: signup tokens/checkout metadata use history attribution, Admin adds
          use context attribution, member reads enrich creation/subscription events, and activity
          feeds map loaded event relations. Staff retains service for notification attribution;
          EmailRenderer retains service and tagger for links. Content API post/page HTML mapping
          retains the root and reads service.outboundLinkTagger per call. HTTP tests pin token data,
          checkout metadata, member read/feed attribution and subscription persistence; staff
          and renderer units use injected fakes. Booted tests additionally pin Staff/Members
          retained identity and EmailRenderer's real attribution/tagger use across a setting
          change. Browser history capture is a separate producer, not another server constructor.
        sources:
          - core/server/services/members/api.js
          - core/server/services/members/members-api/members-api.js
          - core/server/services/members/members-api/controllers/router-controller.js
          - core/server/services/members/members-api/services/member-bread-service.js
          - core/server/services/members/members-api/repositories/event-repository.js
          - core/server/services/staff/index.ts
          - core/server/services/staff/staff-service.js
          - core/server/services/email-service/email-service-wrapper.js
          - core/server/services/email-service/email-renderer.js
          - core/server/api/endpoints/utils/serializers/output/mappers/posts.js
          - core/frontend/src/member-attribution/member-attribution.js
        tests:
          - test/e2e-server/services/member-attribution.test.js
          - test/e2e-api/members/send-magic-link.test.js
          - test/e2e-api/members/create-stripe-checkout-session.test.js
          - test/e2e-api/admin/members.test.js
          - test/e2e-api/members/webhooks.test.js
          - test/unit/server/services/staff/staff-service.test.js
          - test/unit/server/services/email-service/email-renderer.test.js
      startup:
        summary: >-
          Boot loads core models/settings and routing, then awaits memberAttribution.init
          before entering the service group in which Staff, Members and Email capture the ready
          collaborators. Full-app readiness follows that group. Root tests mechanically adopt
          the guarded getter and awaited initializer; existing booted consumer tests keep proving
          ready collaborators after actual boot. Initialization acquires no external resources.
        sources:
          - core/boot.js
          - core/server/services/member-attribution/index.ts
          - core/server/services/staff/index.ts
          - core/server/services/members/api.js
          - core/server/services/email-service/email-service-wrapper.js
        tests:
          - test/unit/server/services/member-attribution/index.test.ts
          - test/e2e-server/services/member-attribution.test.js
          - test/e2e-api/members/send-magic-link.test.js
      lifetime:
        summary: >-
          defineService retains the whole ready graph across repeated init and same-process boots.
          Staff, Members and Email retain that instance; Attribution objects retain their URL
          translator. There is no root history/result cache;
          histories and attributions are created per operation. Event reads query current rows
          ordered created_at then id descending and fetch current resources; missing resources
          degrade to saved URL/null attribution. Booted tests pin deleted resources and current
          post/page/tag/author URLs. Unit event mocks ignore query callbacks, so they do not pin
          the deterministic ordering or concurrent event-write/read behavior. Root and booted
          consumer tests pin retained identity across repeated initialization.
        sources:
          - core/server/services/member-attribution/index.ts
          - core/server/services/member-attribution/member-attribution-service.js
          - core/server/services/member-attribution/attribution-builder.js
          - core/server/services/member-attribution/url-history.js
          - core/server/services/member-attribution/url-translator.js
        tests:
          - test/unit/server/services/member-attribution/index.test.ts
          - test/e2e-server/services/member-attribution.test.js
          - test/unit/server/services/member-attribution/service.test.ts
      configuration:
        summary: >-
          ReferrerParser captures site/admin URLs at init; source-tracking and outbound-tagging
          settings plus tagger site URL are callbacks read during use. Shared URL utilities are
          retained separately and configured frozen; accessor reads do not establish production
          hot reload. Disabling source tracking substitutes empty history/context attribution,
          but does not gate persisted-event reads or addPostAttributionTracking. History rejects
          the whole malformed array, excludes entries at least 24 hours old and iterates reverse
          input order without sorting. Builder prefers latest post, then other identified content,
          then URL; referrer selection uses the latest match and earliest UTM-bearing entry.
          Units pin algorithms, URL/tagging rules and fixed callbacks; booted subdirectory tests
          use test config replacement. Root tests pin same-instance live source-tracking, tagging
          policy and tagger URL reads, plus init-only parser URL reads and captured Admin referrer
          matching; they do not establish production hot reload.
        sources:
          - core/server/services/member-attribution/index.ts
          - core/server/services/member-attribution/member-attribution-service.js
          - core/server/services/member-attribution/referrer-translator.js
          - core/server/services/member-attribution/url-history.js
          - core/server/services/member-attribution/attribution-builder.js
          - core/server/services/member-attribution/outbound-link-tagger.js
          - core/shared/url-utils.ts
        tests:
          - test/unit/server/services/member-attribution/index.test.ts
          - test/unit/server/services/member-attribution/history.test.ts
          - test/unit/server/services/member-attribution/attribution.test.ts
          - test/unit/server/services/member-attribution/referrer-translator.test.ts
          - test/unit/server/services/member-attribution/outbound-link-tagger.test.ts
          - test/e2e-server/services/member-attribution.test.js
      resources:
        summary: >-
          The graph owns parser/translator objects and callback references, with no timers,
          event subscriptions, network client, pending-query registry or cleanup hook. URL/model
          reads borrow shared services/database and accept no caller transaction options. Members'
          repository owns creation-event writes, length fitting and transaction propagation;
          Staff owns notification subscriptions, and email/checkout callers own external effects.
          A real transaction test reads member attribution before commit and another pins long
          UTM truncation; neither proves rollback. History objects carry UTM fields, but this
          service's three event-reading methods rebuild only attribution/referrer fields, omitting
          stored UTM. Tests for UTM persistence do not prove its return on those reader surfaces.
        sources:
          - core/server/services/member-attribution/index.ts
          - core/server/services/member-attribution/member-attribution-service.js
          - core/server/services/member-attribution/referrer-translator.js
          - core/server/services/member-attribution/url-translator.js
          - core/server/services/member-attribution/attribution-builder.js
          - core/server/lib/link-replacer.js
          - core/server/services/members/members-api/repositories/member-repository.js
          - core/server/services/staff/staff-service.js
        tests:
          - test/e2e-api/admin/members.test.js
          - test/e2e-api/members/webhooks.test.js
      failures:
        summary: >-
          Root init rejects with the factory error, leaves the guarded service unavailable and
          permits a later explicit retry. Invalid history normally becomes
          empty; path-resolution rejection degrades to URL attribution, while explicit resource
          lookup, event queries, callbacks and parser failures otherwise propagate. Integration
          lookup exceptions are swallowed; a null integration instead yields Integration: undefined.
          LinkReplacer catches tokenization/replacement errors and returns unchanged HTML, whereas
          direct URL tagging can throw and its initial enabled check lies outside that catch.
          Staff catches failures in fallback event reads and still sends, but provided-attribution
          enrichment failure reaches the subscriber catch and skips that notification. Units pin
          missing-resource fallbacks and HTML preservation; most injected fault/consumer boundaries
          remain untested. Root characterization mechanically adopts rejected init promises,
          preserving original error identity and successful retry after URL configuration or late
          model acquisition failure.
        sources:
          - core/server/services/member-attribution/index.ts
          - core/server/services/member-attribution/member-attribution-service.js
          - core/server/services/member-attribution/url-translator.js
          - core/server/services/member-attribution/outbound-link-tagger.js
          - core/server/services/member-attribution/attribution-builder.js
          - core/server/lib/link-replacer.js
          - core/server/services/staff/staff-service.js
        tests:
          - test/unit/server/services/member-attribution/index.test.ts
          - test/unit/server/services/member-attribution/attribution.test.ts
          - test/unit/server/services/member-attribution/url-translator.test.ts
          - test/unit/server/services/member-attribution/outbound-link-tagger.test.ts
    unverified: []
    blockers:
      - >-
        Root and booted tests now pin guarded pre-init access, awaited publication, repeated init,
        retained Staff/Members references and EmailRenderer attribution/tagging. Broader parser
        fault coverage and a restart/replacement contract are not established by these tests;
        no external initialization resource requires cleanup.
      - >-
        Same-graph root tests now cover valid history/context tracking changes, live outbound
        tagging policy/source URL, captured parser URLs, integration lookup suppression and two
        composition retry boundaries. Callback/parser faults, URL-resolution fallback through
        the root and consumer error handling still need separate coverage.
      - >-
        Pin latest-event ordering with actual competing rows, relation-selection arguments,
        transaction visibility and pending-query ownership. Existing query mocks skip the ordering
        callback, and the first-relation fake ignores the selected model. Keep caller persistence
        and event subscriptions outside the attribution lifecycle; do not infer rollback or drain.
      - >-
        Characterize UTM reader omissions separately from history/checkout/persistence coverage,
        and verify Content API tagging through its real-root consumer. Staff/Members retained
        identity and EmailRenderer tagging now have booted composition checks.
        Preserve URL encoding, blocked-domain/internal-link rules and degradation boundaries;
        no setting reload or expanded output contract is implied by this migration.
  member-welcome-emails:
    kind: directory
    disposition: migrate
    status: audited
    migrated: true
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: complex
      modelImpact: fits
      reason: >-
        Boot initializes the root after the email-address policy and link tracking are ready,
        which it asserts, and before job handlers and automation polling start. The factory builds the
        translator, token provider, mailers, renderer and implementation once and subscribes
        the locale listener last, so a failed attempt leaves nothing to unwind and an explicit
        retry builds a fresh graph. Stripe and the endpoints read the ready instance per call
        and the automations service passes it to both pollers. Welcome content and sender
        refresh stay explicit operations on the instance, and legacy welcome and general
        automation sends keep their separate transports.
      reviewedAt: 8715e47358c171bce60337108ac325cb56bc43e5
      sources:
        - core/server/services/member-welcome-emails/index.ts
        - core/server/services/member-welcome-emails/member-welcome-email-service.js
        - core/server/services/member-welcome-emails/member-welcome-email-renderer.js
        - core/boot.js
        - core/server/services/automations/service.ts
        - core/server/services/automations/welcome-email-automation-poll.ts
        - core/server/api/endpoints/automated-emails.js
        - test/unit/server/services/member-welcome-emails/index.test.ts
        - test/unit/boot/init-services.test.ts
        - test/integration/services/member-welcome-emails.test.js
      dependencies:
        complexity: complex
        reason: >-
          The root wires logging, config, labs, sentry, settings, URL utils, models, the ready
          email-address policy, settings helpers, both mailers, the verify-email template, the
          token provider, helper registration, the ready link tracking instance and a forwarded
          Lexical render and a strict template loader (a closed name-to-file map read
          synchronously from the root) into the implementation and renderer; neither reads its
          own root, the filesystem or initializes a dependency. The renderer imports handlebars,
          the Koenig replacement helper and the shared email-design and finalize files, and the
          implementation imports the delivery address formatter, each under an exact allowance
          with a boundary regression.
        preparation: []
        reviewedAt: b7cad8406041d09d9e4085c5f981f958d454fc76
        sources:
          - core/server/services/member-welcome-emails/index.ts
          - core/server/services/member-welcome-emails/member-welcome-email-service.js
          - core/server/services/member-welcome-emails/member-welcome-email-renderer.js
          - core/server/services/stripe/stripe-service.js
          - core/server/services/automations/poll.ts
          - core/server/services/automations/welcome-email-automation-poll.ts
          - core/server/api/endpoints/automated-emails.js
          - core/server/api/endpoints/automation-email-previews.js
    entryPoints:
      - member-welcome-emails/index.ts
    notes: >-
      Migrated against b7cad8406041d09d9e4085c5f981f958d454fc76. Boot owns one retained instance; the locale listener,
      welcome-content and sender caches live on it, and content/sender refresh remain explicit
      operations. General automation sends still share this implementation; its source TODO
      leaves their eventual owner open. Constants keep their deep import for models, seeders,
      analytics and automations.
    contracts:
      exports:
        summary: >-
          The TypeScript root default-exports a defineService root with init and a guarded
          service getter and nothing else; constants keep their own module. The service is the
          implementation instance with async load/send, automation send, active check,
          preview/test-send and shared-sender edit/verification methods. Welcome/test sends
          resolve undefined; automation send returns the bulk transport result; previews return
          html/plaintext/subject and sender edits return models/meta. The renderer is a separate
          class. Integration tests assert automation result identity and HTTP
          preview/204/verification responses. The root unit test asserts the root exposes only
          init and service, that service throws before init, that welcome sends resolve
          undefined, automation sends return the bulk transport result and sender
          edits/verification return the collection models with sent_email_verification or
          email_verified meta.
        sources:
          - core/server/services/member-welcome-emails/index.ts
          - core/server/services/member-welcome-emails/member-welcome-email-service.js
          - core/server/services/member-welcome-emails/constants.ts
          - core/server/services/member-welcome-emails/member-welcome-email-renderer.js
        tests:
          - test/unit/server/services/member-welcome-emails/index.test.ts
          - test/integration/services/member-welcome-emails.test.js
          - test/e2e-api/admin/automated-emails.test.ts
          - test/e2e-api/admin/automations.test.ts
      construction:
        summary: >-
          The root factory asserts the email-address policy and link tracking are ready,
          creates ghost-namespace i18n, constructs GhostMailer, MailgunClient, a model-backed
          SingleUseTokenProvider and the renderer, passes them with logging, config, labs,
          sentry, settings, URL utils, models, settings helpers and the verify-email template
          into the implementation (which constructs only MagicLink itself), and subscribes to
          settings.locale.edited last. The renderer creates isolated Handlebars, registers the
          injected email helpers, synchronously reads partials/wrapper and compiles the
          template; it reads partials and wrapper through the injected loader, which is the only
          filesystem access and only reaches the six known template files. Production
          constructors are reached only in the root; tests construct the renderer directly with
          the same collaborators. The injected Lexical render forwards to
          the shared facade, which initializes its renderer lazily and retains a separate
          PostsService graph through its deep factory. The root unit test asserts a failed attempt (link tracking not ready)
          rejects without a listener, a later init builds the service with one listener, and
          repeated or concurrent init retains the same instance.
        sources:
          - core/server/services/member-welcome-emails/index.ts
          - core/server/services/member-welcome-emails/member-welcome-email-service.js
          - core/server/services/member-welcome-emails/member-welcome-email-renderer.js
          - core/server/services/members/single-use-token-provider.js
          - core/server/lib/mail/ghost-mailer.js
          - core/server/lib/mailgun/mailgun-client.js
          - core/server/lib/magic-link/magic-link.js
          - core/server/lib/lexical.js
          - core/server/services/posts/posts-service-instance.js
        tests:
          - test/unit/server/services/member-welcome-emails/index.test.ts
      consumers:
        summary: >-
          Automated-email and automation-preview endpoints read the ready service per request
          for previews, tests and sender changes. AutomationsService reads the ready service
          when each poll runs and passes it to both pollers, whose option types are the
          instance's send methods; Stripe's paid-welcome callback reads the service at call
          time because Stripe initializes earlier in boot. Models, member enrollment,
          automation repositories/seeders and analytics deep-import constants only. HTTP tests
          use the real root for selected previews, test sends and shared sender persistence;
          mail is stubbed. Poll tests inject fake instances. The root unit test drives the
          real Stripe checkout callback and all six endpoint operations against a stubbed
          service getter, asserting each forwards the frame to the ready service.
        sources:
          - core/server/api/endpoints/automated-emails.js
          - core/server/api/endpoints/automation-email-previews.js
          - core/server/api/endpoints/automated-email-design.js
          - core/server/services/automations/service.ts
          - core/server/services/automations/poll.ts
          - core/server/services/automations/welcome-email-automation-poll.ts
          - core/server/services/stripe/stripe-service.js
          - core/server/services/stripe/services/webhook/checkout-session-event-service.js
          - core/server/services/member-welcome-emails/index.ts
          - core/server/services/email-analytics/index.ts
          - core/server/services/automations/database-automations-repository.ts
          - core/server/services/members/members-api/repositories/member-repository.js
          - core/server/models/automation.js
          - core/server/models/welcome-email-automated-email.js
          - core/server/data/seeders/importers/automation-action-revisions-importer.ts
          - core/server/data/seeders/importers/automations-importer.ts
        tests:
          - test/unit/server/services/member-welcome-emails/index.test.ts
          - test/e2e-api/admin/automated-emails.test.ts
          - test/e2e-api/admin/automations.test.ts
          - test/integration/services/automations/welcome-email-automation-poll.test.ts
          - test/unit/server/services/automations/poll.test.ts
          - test/unit/server/services/stripe/services/webhooks/checkout-session-event-service.test.js
      startup:
        summary: >-
          Boot awaits the root's init after the services group that includes members and link
          tracking, and before job handlers register and AutomationsService dispatches its
          first poll. Settings, models, mail configuration, the email-address policy and link
          tracking are therefore usable when the factory runs. Legacy polling claims runs then
          calls loadMemberWelcomeEmails; new polling sends without loading legacy content.
          Direct welcome send before loading finds empty slots, whereas active checks and
          previews query models independently. The boot harness asserts init waits for link
          tracking, runs before job registration and automations init, and that its failure
          stops dependent work. Fake-instance poll tests assert load for populated legacy
          batches only. The root unit test asserts a welcome send before any load rejects with
          the missing email message without querying automations or sending.
        sources:
          - core/boot.js
          - core/server/services/automations/service.ts
          - core/server/services/automations/poll.ts
          - core/server/services/automations/welcome-email-automation-poll.ts
          - core/server/services/member-welcome-emails/index.ts
          - core/server/services/stripe/stripe-service.js
          - core/server/api/endpoints/automated-emails.js
          - core/server/api/endpoints/automation-email-previews.js
        tests:
          - test/unit/server/services/member-welcome-emails/index.test.ts
          - test/unit/boot/init-services.test.ts
          - test/integration/services/automations/welcome-email-automation-poll.test.ts
          - test/unit/server/services/automations/poll.test.ts
      lifetime:
        summary: >-
          defineService retains the first successful instance for the process; the graph,
          i18n/listener and compiled templates persist without reset, and a failed attempt
          resets only the helper's state. Each legacy load replaces newsletter sender options then
          sequentially replaces free/paid content, status and design snapshots. A failed or
          overlapping load can leave mixed generations. Sender lookup otherwise memoizes a
          resolved value, including an empty object, without an in-flight guard. Test sends
          refresh the sender cache; automation sends fetch design per call; shared sender
          edits/verification reload response models but do not invalidate private caches.
          An integration assertion changes stored footer/badge after load and confirms the
          old design is sent. Integration helpers reload rather than rebuild the instance. The
          root unit test asserts the welcome content cache is filled only by load and replaced
          by the next load, the newsletter sender memoizes across automation sends until a load
          or test send refreshes it, test sends leave welcome content untouched, automation
          sends look the design up per send, and repeated init retains the instance.
          Concurrent or failed refresh remains unasserted.
        sources:
          - core/server/services/member-welcome-emails/index.ts
          - core/server/services/member-welcome-emails/member-welcome-email-service.js
          - core/server/services/member-welcome-emails/member-welcome-email-renderer.js
        tests:
          - test/unit/server/services/member-welcome-emails/index.test.ts
          - test/integration/services/member-welcome-emails.test.js
      configuration:
        summary: >-
          GhostMailer captures transport/options at construction. Mailgun retains config
          and settings facades, resolving credentials per use. Site title/accent/icon/locale,
          email-address policy and the optional bulk tag are read during use; derived newsletter
          sender strings and welcome designs remain cached. The retained URL facade has
          frozen base URLs. The locale is read once when the root initializes; the locale
          callback changes the retained i18n and the renderer keeps its t and bound dir. There is no root feature gate: automations labs gates unsubscribe
          links; polling supplies tracking preferences and eligibility checks. Tokens use
          24-hour lifetime, 10 minutes after first use and max seven uses. Integration tests
          with stubbed transports assert selected sender fallback/precedence, tags, open
          tracking and unsubscribe payloads. Config stubs are not production hot reload. The
          root unit test asserts the configured locale seeds the translator, a locale edit
          delivered to the listener changes rendered footer text, and site title is read at
          each send. Token-limit wiring remains unasserted here.
        sources:
          - core/server/services/member-welcome-emails/index.ts
          - core/server/services/member-welcome-emails/member-welcome-email-service.js
          - core/server/services/member-welcome-emails/member-welcome-email-renderer.js
          - core/server/services/member-welcome-emails/constants.ts
          - core/server/services/email-address/email-address-service-wrapper.js
          - core/server/lib/mail/ghost-mailer.js
          - core/server/lib/mailgun/mailgun-client.js
          - core/server/services/automations/poll.ts
          - core/shared/url-utils.ts
          - core/shared/config/loader.ts
        tests:
          - test/unit/server/services/member-welcome-emails/index.test.ts
          - test/integration/services/member-welcome-emails.test.js
      resources:
        summary: >-
          The root owns its locale subscription, i18n, implementation, cached content,
          sender options, mail transport, MagicLink and renderer/templates. Models/database,
          email-address, settings, URL, Lexical and the ready link-tracking instance are
          borrowed. Tracked renders can persist automation redirect rows before mail
          acceptance. Mailgun creates an SDK client per use with a 60-second
          timeout. Verification tokens are persisted in the shared token table; members
          cleanup jobs own age-based deletion. The root owns no timer/job and registers no
          mail drain, transport close or listener removal with GhostServer. Renderer unit
          tests assert URL forwarding/output with fake tracking; real renderer snapshots
          cover selected content, not tracking persistence or resource cleanup.
        sources:
          - core/server/services/member-welcome-emails/index.ts
          - core/server/services/member-welcome-emails/member-welcome-email-service.js
          - core/server/services/member-welcome-emails/member-welcome-email-renderer.js
          - core/server/services/email-service/helpers/register-helpers.js
          - core/server/lib/email-rendering/finalize.ts
          - core/server/lib/lexical.js
          - core/server/lib/mail/ghost-mailer.js
          - core/server/lib/mailgun/mailgun-client.js
          - core/server/lib/magic-link/magic-link.js
          - core/server/services/link-tracking/index.js
          - core/server/services/link-tracking/link-click-tracking-service.js
          - core/server/services/link-redirection/link-redirects-service.js
          - core/server/services/link-redirection/link-redirect-repository.js
          - core/server/services/members/single-use-token-provider.js
          - core/server/services/members/jobs/clean-tokens-task.ts
          - core/server/services/members/jobs/index.js
          - core/server/ghost-server.ts
        tests:
          - test/unit/server/services/member-welcome-emails/member-welcome-email-renderer.test.js
          - test/integration/services/member-welcome-emails-snapshot.test.js
      failures:
        summary: >-
          A failed factory run rejects init with the original error and registers no locale
          listener, because the subscription is the last step; partial mailers and templates
          are garbage. Validation, model, rendering and send failures usually propagate;
          only Lexical/URL transformation errors are wrapped as invalid content. Sender edits
          persist allowed fields before verification mail; token usage commits before sender
          update. Neither is an atomic mail/database operation. Legacy polling sends before
          its recipient/run transaction: a real database test with fake send asserts recipient
          rollback and retry when finishing fails. New poll tests with fake dependencies assert
          retry on send failure but continuation on recipient or analytics failure. Unconfigured
          Mailgun returns null; the real send forwards it and new polling can still finish,
          without an acceptance check. Root integration tests assert inactive/missing welcome
          errors and no mail call; renderer tests assert wrapped render rejection. The root
          unit test asserts an unconfigured bulk transport's null result is returned without a
          transactional fallback, a verification-required reply-to sends a link through the
          transactional mailer without persisting, a token for another property is rejected
          after its use is recorded, and an unknown token rejects before any edit, and that a
          not-ready dependency rejects init without a listener. The boot harness asserts an
          init failure stops job registration and automations init. Refresh failure paths
          remain source-only.
        sources:
          - core/server/services/member-welcome-emails/index.ts
          - core/server/services/member-welcome-emails/member-welcome-email-service.js
          - core/server/services/member-welcome-emails/member-welcome-email-renderer.js
          - core/server/lib/mailgun/mailgun-client.js
          - core/server/lib/magic-link/magic-link.js
          - core/server/services/members/single-use-token-provider.js
          - core/server/services/automations/welcome-email-automation-poll.ts
          - core/server/services/automations/poll.ts
        tests:
          - test/unit/server/services/member-welcome-emails/index.test.ts
          - test/unit/boot/init-services.test.ts
          - test/integration/services/member-welcome-emails.test.js
          - test/unit/server/services/member-welcome-emails/member-welcome-email-renderer.test.js
          - test/integration/services/automations/welcome-email-automation-poll.test.ts
          - test/unit/server/services/automations/poll.test.ts
    unverified: []
    blockers:
      - >-
        Define and cover shutdown ownership for the locale listener, i18n, mail transport and
        outstanding sends. Failed-init retry without a stale listener is characterized; no
        root cleanup or shutdown assertion exists and the locale read at init is not refreshed
        by a later boot in the same process.
      - >-
        Cover overlapping/failed welcome loads and edits during send. Current refresh is
        staged, not atomic; memoization, load and test-send refresh and listener-driven locale
        changes are characterized, but coherent sender/content snapshots and invalidation
        policy are not.
      - >-
        Cover verification expiry/reuse limits, concurrent sender edits and mail/database
        failures. Shared MagicLink.transporter is replaced per request before awaiting token
        creation; concurrent calls can use a later sender closure. Generation, the disallowed
        property rejection and the recorded token use before that rejection are characterized;
        root token limits are not.
      - >-
        Decide and enforce the unconfigured bulk-mail/null-result outcome: the characterized
        service returns null without a fallback send and polling can record/finish without
        sending. Protect failure boundaries
        between send, redirect rows, recipient persistence and run completion; legacy retry after
        acceptance can duplicate mail. Provider delivery, restart/reclaim and duplicate prevention
        are not established by stubbed mail or the tested database rollback.
      - >-
        Preserve the separate legacy welcome and general automation send contracts while their
        ownership is decided; the source already identifies general automation sending here as
        misplaced. Transport, tags and per-send design lookup are characterized. Keep
        rendering/sender/verification business logic outside the HTTP layer.
  members:
    kind: directory
    disposition: review
    status: audited
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: complex
      modelImpact: may-change
      reason: >-
        The root retains its API and verification trigger but rebuilds SSR, integrity-token
        and import/export collaborators on each init. Other services capture its repository
        during the same boot group, and nested constructors install listeners. Decide which
        graph becomes ready once, which bindings need explicit refresh, and how failed
        composition unwinds before applying one retained instance.
      reviewedAt: f276513ba32b7114fd5cc4f7b02d3f82c0c31e3e
      sources:
        - core/server/services/members/service.js
        - core/server/services/members/api.js
        - core/server/services/members/members-api/members-api.js
        - core/server/services/members/members-api/services/payments-service.js
        - core/boot.js
      dependencies:
        complexity: complex
        reason: >-
          Composition is spread across service.js, api.js, the nested Members API, import/export and
          jobs; repositories, middleware and helpers still import models, database, events,
          configuration and other services directly. Members captures ready Offers, attribution,
          email-address and metafields, while Stripe/Gifts/Comments retain reciprocal deferred
          access. A root-only DI migration must distinguish these call-time edges from actual
          initialization prerequisites and preserve independently constructed token providers and
          request helpers.
        preparation:
          - >-
            Refactor nested API, import/export and job composition to accept explicit dependencies
            from the root, then remove direct database/model/config/event imports from
            implementations in bounded slices.
          - >-
            Record and test the actual readiness graph for Stripe, Gifts, Comments, Offers,
            newsletters, attribution, suppression and metafields; preserve lazy collaborators
            instead of eagerly resolving every service.
          - >-
            Separate shared token, filtering and verification support from boot-owned state where
            evidence supports it, preserving external constructors and deep helper consumers before
            changing the public root.
        reviewedAt: 44ad405bff4a8700af2ed51f833ebc08d25dddff
        sources:
          - core/server/services/members/service.js
          - core/server/services/members/api.js
          - core/server/services/members/members-api/members-api.js
          - core/server/services/members/members-api/repositories/member-repository.js
          - core/server/services/members/import-export/index.ts
          - core/server/services/members/jobs/index.js
          - core/server/services/members/middleware.js
          - core/server/services/stripe/stripe-service.js
          - core/boot.js
    entryPoints:
      - members/index.js
      - members/jobs/index.js
      - members/single-use-token-provider.js
    notes: >-
      Decide which parts are built once before migrating: the API and verification trigger
      survive repeated init, while SSR, integrity-token and import/export collaborators are
      rebuilt with the secrets and settings they capture. Record the composite facade,
      retained API graph, per-init bindings and separate cleanup jobs together. All facets are source-checked; selected operation tests do not establish a
      uniform initialization, restart or drain contract.
    contracts:
      exports:
        summary: >-
          Source-checked: index re-exports one mutable CommonJS facade with async init, a getter
          returning the existing API, config, stats, middleware, stripeConnect, contentGating
          and createPaidMemberShim. SSR, verification, integrity-token,
          importCSV/importInline/handleImportJob and export start null and are published during
          init. The API is a concrete object, despite callers sometimes awaiting its getter; it
          exposes repositories, account/BREAD services, Express routers, identity/magic-link
          methods, paymentsService and an EventEmitter. The ready event is emitted inside the
          factory before callers receive the bus. Export resolves a readable before its contents
          finish; importCSV distinguishes inline results from accepted deferred work. HTTP
          asserts selected session, signup and import/export behavior, not facade identity or
          complete pre-init compatibility.
        sources:
          - core/server/services/members/index.js
          - core/server/services/members/service.js
          - core/server/services/members/members-api/members-api.js
          - core/server/services/members/import-export/import/importer.ts
          - core/server/services/members/import-export/export/exporter.ts
        tests:
          - test/e2e-api/members/signin.test.js
          - test/e2e-api/members/session-auth.test.ts
          - test/e2e-api/admin/members-import-path-selection.test.js
          - test/e2e-api/admin/members-exporter.test.js
      construction:
        summary: >-
          Source-checked: service import constructs GhostMailer, MembersConfigProvider and
          MembersStats; loading the API factory constructs a second mailer. First init assembles
          token/signing providers, member/product/event repositories,
          BREAD/account/payments/geolocation/MagicLink services and controllers/routers.
          Payments construction subscribes to four offer/tier events; the API subscribes to
          email suppression. VerificationTrigger is constructed once and conditionally
          subscribes to MemberCreatedEvent. Each init builds SSR, integrity and import/export
          collaborators; makeImporter acquires storage:imports and retains a lazily cached
          repository promise. Member jobs separately initialize task closures. Settings,
          newsletters and welcome emails directly construct SingleUseTokenProvider. Deep API
          units stub models, repositories, subscription registration and gift transactions;
          their gift argument/order assertions do not exercise root wiring.
        sources:
          - core/server/services/members/service.js
          - core/server/services/members/api.js
          - core/server/services/members/members-api/members-api.js
          - core/server/services/members/members-api/services/payments-service.js
          - core/server/services/members/members-api/repositories/event-repository.js
          - core/server/services/members/account-service.ts
          - core/server/services/members/import-export/index.ts
          - core/server/services/members/jobs/index.js
          - core/server/services/members/single-use-token-provider.js
          - core/server/services/verification-trigger.js
          - core/server/services/settings/settings-service.js
          - core/server/services/newsletters/index.ts
          - core/server/services/member-welcome-emails/index.ts
          - core/server/lib/mail/ghost-mailer.js
        tests:
          - test/unit/server/services/members/members-api/members-api.test.js
      consumers:
        summary: >-
          Source-checked: boot, Admin/member HTTP endpoints, frontend sessions/JWKS,
          unsubscribe, previews/gift access and other services retain the facade. Most read
          api/ssr at use time; Stripe and gifts use member getters, while email sending and
          analytics capture the repository after members.init is invoked. Checkout previews
          resolve api.paymentsService per request and pass the previewed checkout to
          getPreviewLink. The preview HTTP test asserts an unsaved design reaches the mocked
          Stripe session without saving it; it does not test replacement of the members API.
          Auth lazily captures public issuer/key-access configuration. Deep consumers include
          cleanup/job classes, token providers, gating, serializers, avatar and commenting
          helpers. Revue import calls importInline without forwarding its outer transaction.
          HTTP routes remain split between web modules and routers constructed inside
          MembersAPI. Real HTTP tests assert signed-in reads/edits, anonymous 204/401 and
          unchanged data, magic-link signup rows/cookies, import 201/202 with mocked completion
          mail, and CSV fields/filtering. Those assertions do not prove every retained consumer
          survives dependency replacement.
        sources:
          - core/boot.js
          - core/server/api/endpoints/members.js
          - core/server/api/endpoints/members-account.ts
          - core/server/api/endpoints/member-commenting.ts
          - core/server/api/endpoints/member-signin-urls.js
          - core/server/api/endpoints/members-stripe-connect.js
          - core/server/api/endpoints/settings.js
          - core/server/api/endpoints/exports.js
          - core/server/api/endpoints/previews.js
          - core/server/api/endpoints/utils/gift-link-access.ts
          - core/server/web/members/app.js
          - core/server/web/members/account/routes.ts
          - core/server/web/comments/routes.js
          - core/server/auth/members/index.js
          - core/server/services/stripe/stripe-service.js
          - core/server/services/gifts/index.ts
          - core/server/services/members-events/index.js
          - core/server/services/email-service/email-service-wrapper.js
          - core/server/services/webhooks/index.js
          - core/server/services/comments/index.ts
          - core/server/services/explore-ping/index.ts
          - core/frontend/web/site.js
          - core/frontend/services/proxy.js
          - core/frontend/services/routing/controllers/unsubscribe.js
          - core/server/services/email-service/email-renderer.js
          - core/server/models/member.js
          - core/server/services/webhooks/serialize.js
          - core/server/api/endpoints/utils/serializers/output/members.js
          - core/server/api/endpoints/utils/serializers/output/members-csv-transform.js
          - core/server/data/importer/importers/data/revue-subscriber-importer.js
          - core/server/services/jobs-service/register-job-handlers.ts
          - core/server/services/stripe-checkout-config/index.ts
          - core/server/services/stripe-checkout-config/preview-service.ts
          - core/server/services/members/members-api/services/payments-service.js
        tests:
          - test/e2e-api/members/session-auth.test.ts
          - test/e2e-api/members/signin.test.js
          - test/e2e-api/admin/members-import-path-selection.test.js
          - test/e2e-api/admin/members-exporter.test.js
          - test/e2e-api/admin/stripe-checkout-preview.test.ts
      startup:
        summary: >-
          Source-checked: settings, signing keys, i18n, metafields and the Stripe Checkout config,
          whose checkout service the members API reads when it is built, precede members; boot
          awaits Stripe and member-attribution and initializes email-address/gifts before the
          parallel service group.
          Members publishes its graph synchronously before awaiting historical migrations, so
          later group expressions already read api.members. Tiers is initialized afterward in
          that group; member and router controllers retain its wrapper and read its API during
          operations. Jobs tasks/handlers are installed after the group, before jobs.start;
          cleanup schedules run in background and independently log failures. Root init rejects
          prohibited Stripe environment/URL combinations. Real boot migration tests assert a
          finished row, skip on an existing finished row, retry of a failed row and failure
          handling with stubbed migration execution; they do not hold root initialization or
          exercise a real Stripe backfill. Cleanup units assert pre-init task guards and
          selected schedules.
          Historical evidence removed after auditedAt: [Members migration tests](https://github.com/TryGhost/Ghost/blob/bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e/ghost/core/test/e2e-server/services/members-migrations.test.ts).
        sources:
          - core/boot.js
          - core/server/services/members/service.js
          - core/server/services/members/api.js
          - core/server/services/members/members-api/members-api.js
          - core/server/services/members/members-api/controllers/router-controller.js
          - core/server/services/members/members-api/controllers/member-controller.js
          - core/server/services/members/members-api/services/payments-service.js
          - core/server/services/members/jobs/index.js
          - core/server/services/jobs-service/register-job-handlers.ts
        tests:
          - test/unit/server/services/members/jobs/cleanup-tasks.test.ts
          - test/unit/server/services/members/jobs/schedule-token-cleanup.test.ts
          - test/unit/server/services/members/jobs/schedule-expired-comped-cleanup.test.ts
      lifetime:
        summary: >-
          Source-checked: API and verification instances survive repeated init while SSR,
          integrity provider and import/export closures are replaced. API-held Stripe, offers,
          attribution, email-address and metafields instances remain captured; settings and
          selected service wrappers are read later. No root init promise, rollback, reset or
          shutdown coordinates concurrent calls or partial publication. Failed factory
          construction can leave earlier payment listeners installed. Importers retain the first
          repository promise, including rejection; existing in-flight operations keep their old
          dependencies. Cleanup task closures and successful schedule flags persist
          independently; concurrent scheduling is not promise-coalesced. Repeat migration tests
          pin database-run guards, not these identities. Expired-comped scheduling units assert
          two sequential calls schedule once; token scheduling only checks a single production
          call and test suppression.
          Historical evidence removed after auditedAt: [Members migration tests](https://github.com/TryGhost/Ghost/blob/bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e/ghost/core/test/e2e-server/services/members-migrations.test.ts).
        sources:
          - core/server/services/members/service.js
          - core/server/services/members/api.js
          - core/server/services/members/members-api/members-api.js
          - core/server/services/members/members-api/services/payments-service.js
          - core/server/services/members/import-export/index.ts
          - core/server/services/members/jobs/index.js
        tests:
          - test/unit/server/services/members/jobs/schedule-token-cleanup.test.ts
          - test/unit/server/services/members/jobs/schedule-expired-comped-cleanup.test.ts
      configuration:
        summary: >-
          Source-checked: mail transport/options and stats dialect are captured at import; API
          issuer, signing provider and OTC secret at first construction; SSR cookie
          key/path/security and integrity secret at each init. Config-provider callbacks read
          current settings/helpers, while shared URL utilities hold frozen URLs. Import
          threshold and verification callbacks call the config facade; timezone, signup access,
          mail text and payment settings are read at use time. This is not evidence of
          production hot reload. Config units assert issuer/signin URL strings using a replaced
          URL helper; threshold units assert fallback/coercion; integrity units assert HMAC
          shape, expiry and invalid signatures. Import HTTP changes test config to select paths,
          not a production config-reload contract.
        sources:
          - core/server/services/members/service.js
          - core/server/services/members/api.js
          - core/server/services/members/members-config-provider.js
          - core/server/services/members/members-api/services/payments-service.js
          - core/server/services/members/members-ssr.js
          - core/server/services/members/request-integrity-token-provider.js
          - core/server/services/members/members-api/services/token-service.js
          - core/server/services/members/middleware.js
          - core/server/services/members/import-export/config.ts
          - core/server/services/members/stripe-connect.js
          - core/server/lib/mail/ghost-mailer.js
          - core/shared/url-utils.ts
        tests:
          - test/unit/server/services/members/config.test.js
          - test/unit/server/services/members/import-export/config.test.ts
          - test/unit/server/services/members/request-integrity-token-provider.test.ts
          - test/e2e-api/admin/members-import-path-selection.test.js
      resources:
        summary: >-
          Source-checked: members borrows DB/signing/Stripe/storage/jobs resources, owns two
          mailers and retained domain-event listeners, and creates per-operation token
          transactions, CSV streams, import spools and row transactions. Token consumption is
          separate from subsequent member/login/gift work. Import rows commit independently;
          Stripe effects are outside database rollback. Dispatch failure attempts spool removal;
          handled imports await removal and completion mail with reporting catches. Jobs owns
          queued work/drain; HTTP pipelines own export streams, and no members shutdown drains
          mail, listeners or detached post-commit work. DomainEvents catches listener
          rejections; its allSettled tracking is test-only. Geolocation uses Got with a 500ms
          request timeout, disabling retries only in tests. Local spool tests prove
          cross-instance file read/removal; cleanup integration tests use actual jobs/DB and a
          real transaction with injected final-write failure. Export HTTP asserts CSV content;
          serializer units forward an injected Writable failure, not real DB/socket
          cancellation.
        sources:
          - core/server/services/members/members-api/members-api.js
          - core/server/services/members/members-api/services/payments-service.js
          - core/server/services/verification-trigger.js
          - core/server/services/members/single-use-token-provider.js
          - core/server/services/members/members-api/repositories/member-repository.js
          - core/server/lib/after-commit.ts
          - core/server/services/members/members-api/services/geolocation-service.js
          - core/server/services/members/import-export/import/importer.ts
          - core/server/services/members/import-export/import/spool.ts
          - core/server/services/members/import-export/csv/parse.ts
          - core/server/services/members/import-export/export/exporter.ts
          - core/server/services/members/jobs/clean-tokens-task.ts
          - core/server/services/members/jobs/clean-expired-comped-task.ts
          - core/server/services/jobs-service/jobs-service.ts
          - core/server/api/endpoints/utils/serializers/output/stream-response.ts
          - core/server/data/exporter/site-exporter.ts
          - core/server/lib/mail/ghost-mailer.js
          - package.json
        tests:
          - test/unit/server/services/members/import-export/import/spool.test.ts
          - test/integration/services/members/clean-tokens.test.js
          - test/integration/services/members/clean-expired-comped.test.ts
          - test/unit/api/endpoints/utils/serializers/output/members-export-csv.test.ts
          - test/e2e-api/admin/members-exporter.test.js
      failures:
        summary: >-
          Source-checked: import/construction and migration-row read/write failures can reject
          init after partial publication; migration execution failure is logged and recorded
          failed without rejecting. Any existing non-failed row skips; failed insertion is
          swallowed when a row can subsequently be found. API router forwardError wrappers
          forward async errors; JWKS instead directly returns a logging/rethrowing handler.
          Session loading/token endpoints deliberately suppress broader failures; identity
          loading forwards errors other than Ghost BadRequestError. MagicLink persists before
          sending mail, and row/Stripe/mail effects are not one transaction. Import handler
          reports setup/cleanup/mail failures and resolves; inline setup rejects, row failures
          become results and post-write bookkeeping failures preserve results. Migrations tests
          assert those injected runner outcomes. Import units assert exact results/reports with
          fake transactions and spool/mail/Stripe; repository units hold a fake commit promise
          to prove Stripe email-update dispatch waits and is skipped when that promise rejects.
          Neither proves provider rollback or production event drain.
          Historical evidence removed after auditedAt: [Members migration tests](https://github.com/TryGhost/Ghost/blob/bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e/ghost/core/test/e2e-server/services/members-migrations.test.ts).
        sources:
          - core/server/services/members/service.js
          - core/server/services/members/members-api/members-api.js
          - core/server/services/members/members-api/controllers/well-known-controller.js
          - core/server/services/members/middleware.js
          - core/server/services/members/members-ssr.js
          - core/server/lib/magic-link/magic-link.js
          - core/server/services/members/members-api/repositories/member-repository.js
          - core/server/lib/after-commit.ts
          - core/server/services/members/account-service.ts
          - core/server/services/members/import-export/index.ts
          - core/server/services/members/import-export/import/importer.ts
        tests:
          - test/unit/server/services/members/import-export/import/error-handling.test.ts
          - test/unit/server/services/members/members-api/repositories/member-repository.test.js
    unverified: []
    blockers:
      - >-
        Protect the actual facade/import graph, pre-init exports, captured consumers and
        synchronous publication before changing init. Hold/fail dependencies and migrations to
        distinguish graph availability from init completion. Pin retained API/verification
        versus replaced SSR/import/export state, partial-init retry and concurrent init;
        existing migration-row tests do not cover these lifecycle contracts.
      - >-
        Decide ownership/disposal for payment/suppression/verification subscriptions and two
        mail transports. No root stop removes or drains them; API construction failure can leave
        subscriptions before publication. Preserve conditional verification subscription and
        error policy. Event dispatch and test-only allSettled do not make production subscriber
        work durable or awaitable by callers. The shared afterCommit helper gates on
        executionPromise fulfillment; the fake rejected-promise cases do not establish actual
        Knex manual-rollback behavior.
      - >-
        Preserve separate publication, token, member, gift, Stripe and mail boundaries. Token
        consumption precedes member/login work, account unsuppression performs two separate
        writes, and post-commit effects can fail after the caller completes. Real cleanup
        rollback evidence is specific to the cleanup task; fake API/import transactions do not
        establish whole-workflow rollback or concurrent token locking.
      - >-
        Characterize queued import restart/redelivery, partial spool writes/removal failure,
        repeated handler invocation and shutdown while mail/rows are pending. A JSON spool and
        serializable job do not by themselves establish durable delivery. Revue import uses
        independent row transactions and retains its converted CSV; no outer import transaction
        or members cleanup owns that file in the inspected path. Characterize the directly
        mounted JWKS rejection path separately from the forwardError-wrapped member routers
        before changing HTTP composition.
      - >-
        Protect stream acquisition/failure/abort with the real exporter and database before
        changing resource ownership; current CSV content and fake Writable tests do not
        demonstrate pool release under disconnect. Pin cleanup scheduling retry/concurrency and
        restart flags; successful scheduling flags survive independently of the jobs backend,
        and members exposes no cancellation/reset for them.
      - >-
        Preserve Stripe environment checks and mixed config capture: immutable config/URL
        values, captured mail/issuer/secrets/dialect and request-time settings access are
        different contracts. Direct token-provider constructors in settings/newsletters/ welcome
        emails remain separate owners. Keep these members obligations scoped to its migration;
        unrelated member business changes are not pilot prerequisites.
  members-events:
    kind: directory
    disposition: migrate
    status: audited
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: complex
      modelImpact: fits
      reason: >-
        One retained cache/updater pair fits defineService, but the current DomainEvents API
        wraps callbacks without exposing a disposer. Establish a removable subscription
        boundary before promising cleanup and retry after partial registration. Preserve the
        live members-API getter, optional click listener and existing cache identity; this
        is dependency ownership work rather than a need for kernel cleanup hooks.
      reviewedAt: f276513ba32b7114fd5cc4f7b02d3f82c0c31e3e
      sources:
        - core/server/services/members-events/index.js
        - core/server/services/members-events/last-seen-at-updater.js
        - core/server/services/members-events/last-seen-at-cache.js
        - core/boot.js
      dependencies:
        complexity: straightforward
        reason: >-
          The updater/cache already receive database, events, settings, config and a live Members
          API getter from the root. Remaining imports are logging, event constructors and Moment
          Timezone, which can use root binding plus the shared clock. Preserve the getter instead of
          capturing Members during construction; subscription disposal remains separate lifecycle
          work.
        preparation:
          - >-
            Inject logging and event registration capabilities or event constructors from the root,
            and use the shared clock's timezone-capable legacy Moment export in both updater and
            cache.
          - >-
            Keep the call-time Members API getter, live timezone reads and construction-time
            click-listener policy distinct.
        reviewedAt: 44ad405bff4a8700af2ed51f833ebc08d25dddff
        sources:
          - core/server/services/members-events/index.js
          - core/server/services/members-events/last-seen-at-updater.js
          - core/server/services/members-events/last-seen-at-cache.js
          - core/kernel/clock.ts
    entryPoints:
      - members-events/index.js
    notes: >-
      Preserve guarded subscriptions, live members API lookup and the distinct page/click,
      email-open and comment update paths. Establish partial-init recovery, listener ownership,
      transaction/event ordering and cache behavior before changing lifecycle.
    contracts:
      exports:
        summary: >-
          CommonJS exports one eager wrapper with receiver-dependent synchronous init and
          clearLastSeenAtCache, both returning undefined. After init, lastSeenAtCache and
          lastSeenAtUpdater are mutable public properties. Deep CommonJS classes are constructible;
          update methods resolve undefined after work; subscribe and cache methods are synchronous.
          No readiness/reset/shutdown API exists. Deep tests cover selected methods; complete
          facade shape, return values and detached invocation are source-only.
        sources:
          - core/server/services/members-events/index.js
          - core/server/services/members-events/last-seen-at-updater.js
          - core/server/services/members-events/last-seen-at-cache.js
        tests:
          - test/unit/server/services/members-events/last-seen-at-updater.test.js
          - test/unit/server/services/members-events/last-seen-at-cache.test.js
      construction:
        summary: >-
          Boot is the only production init caller found. First init creates a cache with shared
          settings, publishes it, constructs/publishes an updater borrowing config, database and
          common events, then subscribes. A callback reads members.api when needed rather than
          capturing that implementation. Cache construction reads the current day/timezone; updater
          construction requires a truthy members getter and otherwise stores dependencies, creating
          a fallback cache only for direct construction without one. Neither constructor queries
          the database. Unit tests inject collaborators and pin the missing-getter error, not the
          root graph or partial construction.
        sources:
          - core/boot.js
          - core/server/services/members-events/index.js
          - core/server/services/members-events/last-seen-at-updater.js
          - core/server/services/members-events/last-seen-at-cache.js
        tests:
          - test/unit/server/services/members-events/last-seen-at-updater.test.js
          - test/unit/server/services/members-events/last-seen-at-cache.test.js
      consumers:
        summary: >-
          Runtime use is through shared DomainEvents: frontend middleware dispatches page views,
          link tracking dispatches clicks after writes or transaction executionPromise fulfillment,
          comments dispatches after notification work, and the email processor dispatches opens
          before awaiting open-event storage. Page/click writes manually emit common member.edited,
          reaching webhooks; email SQL bypasses model events. Tests directly access the updater and
          clear the cache. Real frontend/comment HTTP tests settle events and pin selected timestamp
          changes; click HTTP pins a valid timestamp and intercepted webhook payload. Processor units
          inject dispatch/storage fakes; real provider-open to member-update composition is unproven.
        sources:
          - core/frontend/web/site.js
          - core/server/services/link-tracking/link-click-repository.js
          - core/server/services/comments/comments-service.js
          - core/server/services/email-service/email-event-processor.js
          - core/server/services/email-service/events/email-opened-event.ts
          - core/server/services/members-events/last-seen-at-updater.js
          - core/server/services/webhooks/listen.js
          - core/shared/events/index.js
        tests:
          - test/e2e-frontend/members.test.js
          - test/e2e-api/members-comments/comments.test.js
          - test/e2e-server/click-tracking.isolated.test.js
          - test/unit/server/services/email-service/email-event-processor.test.js
      startup:
        summary: >-
          Foreground boot calls synchronous init in the service Promise.all argument list after
          members.init is invoked, before newsletter analytics setup and HTTP readiness. Subscribing
          does not read members.api or validate database readiness; the getter is used on later
          page/click/comment work. Email updates use the borrowed database directly. Before init,
          public collaborators are absent and no handlers exist. Booted tests prove usable completed
          composition, not exact ordering, early events, failed publication or retry after failure.
        sources:
          - core/boot.js
          - core/server/services/members-events/index.js
          - core/server/services/members-events/last-seen-at-updater.js
        tests:
          - test/integration/services/last-seen-at-updater.test.js
          - test/e2e-frontend/members.test.js
      lifetime:
        summary: >-
          Truthy lastSeenAtUpdater makes later init a no-op, retaining its dependencies, cache and
          anonymous subscriptions across same-process boots. Replacing wrapper properties does not
          retarget subscribed closures. The members getter observes the current API and db.knex is
          accessed per operation. Cache clear drops IDs only; it neither changes listeners nor resets
          the remembered day. Cache membership expires lazily when checked after the current day
          changes. Tests pin cache clearing/rollover and direct-root daily suppression, not wrapper
          identity, replacement, repeated boot or in-flight shutdown.
        sources:
          - core/server/services/members-events/index.js
          - core/server/services/members-events/last-seen-at-updater.js
          - core/server/services/members-events/last-seen-at-cache.js
          - core/server/data/db/index.js
          - core/server/ghost-server.ts
        tests:
          - test/unit/server/services/members-events/last-seen-at-cache.test.js
          - test/integration/services/last-seen-at-updater.test.js
      configuration:
        summary: >-
          Only explicit false disables click subscription, selected once during subscribe; other
          event types remain registered. Timezone is read per update/cache check. Cache and email SQL
          default missing timezone to Etc/UTC; page/comment paths pass the returned value directly
          to moment. Cache days follow the current clock, while writes compare the event timestamp's
          day. Config is retained as an accessor, not a subtree; no production hot reload is implied.
          Units pin disabled/missing-config click delivery and selected timezone branches. Real DB
          integration pins CET midnight and same-day suppression with direct updater calls, not
          delayed/out-of-order events, DST, same-instance timezone changes or later config replacement.
        sources:
          - core/server/services/members-events/last-seen-at-updater.js
          - core/server/services/members-events/last-seen-at-cache.js
          - core/shared/config/validated.ts
        tests:
          - test/unit/server/services/members-events/last-seen-at-updater.test.js
          - test/unit/server/services/members-events/last-seen-at-cache.test.js
          - test/integration/services/last-seen-at-updater.test.js
      resources:
        summary: >-
          The graph owns a Set of member IDs and three or four DomainEvents handlers with no
          unsubscribe, timer, transport, pending-work registry or shutdown drain. Page/click work
          preemptively caches the ID, locks/rereads the row in a shared-database transaction, refreshes
          relations and saves, then emits member.edited before the transaction completes. Concurrent
          cached calls skip without awaiting the first write. Email opens use conditional raw SQL;
          comments read then update both fields if either is stale, without a shared transaction or
          monotonicity guard. Unit race tests program fake reads/transactions; real integration
          pins one update invocation under a shared cache, not independent-connection locking or
          rollback. DomainEvents allSettled is test tracking, not production drain.
        sources:
          - core/server/services/members-events/index.js
          - core/server/services/members-events/last-seen-at-updater.js
          - core/server/services/members-events/last-seen-at-cache.js
          - core/server/services/members/members-api/repositories/member-repository.js
          - core/server/ghost-server.ts
          - package.json
        tests:
          - test/unit/server/services/members-events/last-seen-at-updater.test.js
          - test/integration/services/last-seen-at-updater.test.js
      failures:
        summary: >-
          Root initialization throws synchronously. Failure before updater publication permits retry;
          subscription failure leaves a truthy updater and potentially partial listeners that later
          init will not repair. Page/click transaction failures evict the cached ID and reject; cache
          checks and preliminary timezone calculations can fail before that catch. Event handlers
          catch/log errors, and DomainEvents independently catches rejected handlers. Direct updater
          calls reject. Existing error-listener tests assert true without logging assertions; the
          retry test checks a second invocation after a synchronous fake transaction throw but does
          not await assert.rejects. Real comment HTTP pins same-day no-op, not concurrent/mixed-date
          writes, partial persistence or notification-before-event failure.
        sources:
          - core/server/services/members-events/index.js
          - core/server/services/members-events/last-seen-at-updater.js
          - core/server/services/members-events/last-seen-at-cache.js
          - core/server/services/comments/comments-service.js
          - package.json
        tests:
          - test/unit/server/services/members-events/last-seen-at-updater.test.js
          - test/e2e-api/members-comments/comments.test.js
    unverified: []
    blockers:
      - >-
        Add root export/identity, initial ordering, partial subscription failure, retry and
        repeated-boot tests. Specify subscriber ownership, unsubscribe and pending-work drain;
        cache clearing is not a lifecycle reset.
      - >-
        Characterize retained callbacks against members/database replacement, click-config
        capture, timezone changes, DST and delayed/out-of-order events. Cache and event days can
        differ; comments can overwrite a newer field when the other field is stale.
      - >-
        Establish real concurrent row locking, failed-write cache eviction and retry, skipped
        in-flight calls, and member.edited delivery relative to commit/rollback. Fake race and
        transaction tests do not prove those database/event guarantees.
      - >-
        Replace assert-true listener failure evidence with observable logs/state and awaited
        rejection assertions. Cover real email-open composition, raw-SQL no-webhook behavior,
        producer failures and comment mixed-date/concurrent updates without changing baseline
        behavior during the initialization migration.
  members-metafields:
    kind: directory
    disposition: migrate
    status: audited
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: straightforward
      modelImpact: fits
      reason: >-
        Boot already builds definitions, values and bindings together before their
        consumers; their constructors only store dependencies. Publish the completed
        three-part facade atomically through defineService and update consumers of the named
        instance exports. Preserve bindings' reference to values, the shared field source, live
        limit getters and the separate constant/type exports.
      reviewedAt: f276513ba32b7114fd5cc4f7b02d3f82c0c31e3e
      sources:
        - core/server/services/members-metafields/index.ts
        - core/server/services/members-metafields/definitions-service.ts
        - core/server/services/members-metafields/bindings-service.ts
        - core/server/services/members/service.js
        - core/boot.js
      dependencies:
        complexity: complex
        reason: >-
          The three service instances are already composed together, but runtime metafield
          schemas/identity functions are imported throughout static codecs, access rules and
          externally consumed helpers. Definitions and values also read database schema at module
          load, config reads defaults at import, and query/key helpers load external libraries.
          Under the current allowlist this is broader than constructor wiring, even though lifecycle
          composition is simple.
        preparation:
          - >-
            Review exact @tryghost/metafield-types and /identity contract allowances, plus
            case-keys, independently of migration; without them introduce explicit helper factories
            rather than hidden global dependency initialization.
          - >-
            Supply logging, database column metadata, key/query helpers, configured/default limit
            resolution and the host limit service from index.ts, preserving the live limit getter
            and the field source asking which namespaces are unavailable on each use.
          - >-
            Keep importable access/filter/schema contracts separate from ready instances and adapt
            the model, API, Members and checkout consumers without forcing them to initialize this
            service just to use a schema or constant.
        reviewedAt: 44ad405bff4a8700af2ed51f833ebc08d25dddff
        sources:
          - core/server/services/members-metafields/index.ts
          - core/server/services/members-metafields/access.ts
          - core/server/services/members-metafields/schema.ts
          - core/server/services/members-metafields/config.ts
          - core/server/services/members-metafields/definitions-service.ts
          - core/server/services/members-metafields/values-service.ts
          - core/server/services/members-metafields/queries.ts
          - core/server/services/members-metafields/key.ts
          - core/server/models/member.js
          - core/server/services/stripe-checkout-config/service.ts
    entryPoints:
      - members-metafields/index.ts
    notes: >-
      Preserve the three boot-wired services, retained consumer identities, explicit write
      audiences and shared transactions. Characterize initialization and failure boundaries
      before changing composition; recorded contracts are not all regression-tested.
    contracts:
      exports:
        summary: >-
          Named runtime exports are synchronous init, mutable namespaces/fields/definitions/values/bindings,
          audience/access constants, canWrite, actingContext and adminWriteOrigin. fields is the
          field source every read of a field goes through, reading a field in an unavailable
          namespace as archived. Service
          bindings are undefined before init; class methods depend on their receiver. Namespaces
          resolves a namespace's policy (owner and limits) and refuses what it doesn't allow. Definitions
          exposes asynchronous CRUD/reorder and addOne/find/recordCreated helpers; values
          exposes synchronous unwrapWire plus asynchronous read/plan/apply/feed methods; bindings exposes asynchronous
          bind/remove/planning. HTTP tests assert selected serialized definitions, values, access
          rules and provenance; they do not pin the root export shape or init return timing.
        sources:
          - core/server/services/members-metafields/index.ts
          - core/server/services/members-metafields/access.ts
          - core/server/services/members-metafields/actions.ts
          - core/server/services/members-metafields/namespaces.ts
          - core/server/services/members-metafields/definitions-service.ts
          - core/server/services/members-metafields/values-service.ts
          - core/server/services/members-metafields/bindings-service.ts
        tests:
          - test/e2e-api/admin/member-custom-fields.test.ts
          - test/e2e-api/members/custom-fields.test.ts
      construction:
        summary: >-
          Init obtains the shared Knex getter, models and config, then publishes namespaces,
          fields, definitions, values and bindings sequentially; bindings retains that values instance.
          Namespaces is built from custom fields' policy, which holds the host limit service and a
          ceiling callback, both asked on each use.
          Definitions receives an action-recording closure over models.Action and the namespaces;
          values receives the ceiling callback; all three receive the field source. Constructors
          store dependencies without querying or starting work.
          The shared DB getter can create the module-cached Knex client if not already loaded.
          No other production construction of these classes was found. Binding units construct
          the class with fake query/values collaborators, not the root or publication sequence.
        sources:
          - core/server/services/members-metafields/index.ts
          - core/server/services/members-metafields/namespaces.ts
          - core/server/services/members-metafields/custom-fields.ts
          - core/server/services/members-metafields/definitions-service.ts
          - core/server/services/members-metafields/values-service.ts
          - core/server/services/members-metafields/bindings-service.ts
          - core/server/data/db/index.js
          - core/server/data/db/connection.ts
        tests:
          - test/unit/server/services/members-metafields/bindings-service.test.ts
      consumers:
        summary: >-
          Admin/member endpoints use definitions; members retains values for BREAD, account,
          member deletion and activity feeds, and definitions/values for CSV import/export.
          Webhooks read values through members BREAD. The Stripe Checkout config reads fields
          through the exported field source. Member filtering and API serialization
          also deep-import helpers. No production calls to the exported bindings service were
          found in the inspected Core source after checkout collection was removed; it remains
          constructed and exported. Real HTTP asserts field/value visibility, CSV cells/import
          provenance, filter matches and filtered bulk actions. Binding units exercise planning
          with fake query/value collaborators, not a production consumer.
        sources:
          - core/server/api/endpoints/member-metafields.ts
          - core/server/api/endpoints/member-metafields-members.ts
          - core/server/api/endpoints/members.js
          - core/server/api/endpoints/utils/serializers/output/member-metafields.ts
          - core/server/services/members/api.js
          - core/server/services/members/members-api/members-api.js
          - core/server/services/members/members-api/services/member-bread-service.js
          - core/server/services/members/members-api/repositories/member-repository.js
          - core/server/services/members/members-api/repositories/event-repository.js
          - core/server/services/members/account-service.ts
          - core/server/services/members/service.js
          - core/server/services/members/import-export/index.ts
          - core/server/services/webhooks/index.js
          - core/server/models/member.js
          - core/server/services/members-metafields/filter.ts
          - core/server/services/members-metafields/index.ts
        tests:
          - test/e2e-api/admin/member-custom-fields.test.ts
          - test/e2e-api/members/custom-fields.test.ts
          - test/e2e-api/admin/members-import-custom-fields.test.ts
          - test/e2e-api/admin/members-exporter-custom-fields.test.ts
          - test/e2e-api/admin/members-filter-custom-fields.test.ts
          - test/unit/server/services/members-metafields/bindings-service.test.ts
      startup:
        summary: >-
          Boot calls init during initCore after database/settings/signing-key/i18n setup and
          before members/tiers composition. Initialization has no feature gate, database-ready
          validation or rollback. API callers assume exported instances exist. Definition reads
          remain available when direct definition writes are blocked by Labs or a namespace's
          limits.
          HTTP tests assert those direct route responses, not dependency ordering, pre-init
          calls or startup faults. Shipped ceiling validation runs at import.
        sources:
          - core/boot.js
          - core/server/services/members-metafields/index.ts
          - core/server/services/members-metafields/config.ts
          - core/server/web/api/endpoints/admin/routes.js
          - core/server/web/members/account/routes.ts
          - core/server/api/endpoints/member-metafields.ts
        tests:
          - test/e2e-api/admin/member-custom-fields.test.ts
          - test/e2e-api/members/custom-fields.test.ts
      lifetime:
        summary: >-
          Once all five bindings exist, init preserves them and their captured Knex/dependencies.
          There is no reset or shutdown hook. If construction throws after an assignment, a retry
          replaces earlier instances; retained members/CSV references would not follow that
          replacement. Definitions, bindings, values and change history
          live in tables, not an instance cache. Archiving hides values while retaining rows;
          deletion cascades values/bindings, and field-history names survive definition deletion.
          HTTP asserts selected archive/delete/readback outcomes, not identity, restart or retry.
        sources:
          - core/server/services/members-metafields/index.ts
          - core/server/services/members-metafields/values-service.ts
          - core/server/services/members-metafields/bindings-service.ts
          - core/server/services/members/service.js
          - core/server/data/schema/schema.js
        tests:
          - test/e2e-api/admin/member-custom-fields.test.ts
          - test/e2e-api/members/custom-fields.test.ts
      configuration:
        summary: >-
          Limit callbacks read members:metafields:maxDefinitions when adding custom fields or
          planning value writes; the shipped fallback is captured and validated at import.
          The parser accepts nonnegative safe integers/numeric strings, including zero, and
          falls back to the shipped 100 for invalid input. Adding counts the namespace's active and
          archived rows; lowering the limit does not remove them. HTTP asserts zero/string/fallback and
          next-request changes using test config mutation, not production hot reload of the
          immutable config tree. Constructors retain Knex; schema/type/key rules are imported.
        sources:
          - core/server/services/members-metafields/index.ts
          - core/server/services/members-metafields/config.ts
          - core/server/services/members-metafields/custom-fields.ts
          - core/server/services/members-metafields/definitions-service.ts
          - core/server/services/members-metafields/values-service.ts
          - core/server/services/members-metafields/schema.ts
          - core/shared/config/defaults.json
          - core/shared/config/SCHEMA.md
        tests:
          - test/e2e-api/admin/member-custom-fields.test.ts
      resources:
        summary: >-
          The root owns no timers, subscriptions, queue, transport or database pool; it borrows
          shared Knex. Definition add/reorder use transactions; addOne and value applyWrite join
          a supplied transaction or open one, while bind/remove use the supplied executor.
          Value changes and their history rows share a transaction; the members service
          composes larger writes around it, and CSV import uses per-row transactions. Definition actions
          are separate post-write work. Real HTTP proves batch rollback on a later name conflict,
          successful value/history writes and cascades; validation refusals do not prove rollback
          after a value/history write fails. Import failure units use fake commit/rollback.
        sources:
          - core/server/services/members-metafields/definitions-service.ts
          - core/server/services/members-metafields/values-service.ts
          - core/server/services/members-metafields/bindings-service.ts
          - core/server/services/members-metafields/actions.ts
          - core/server/services/members/members-api/services/member-bread-service.js
          - core/server/services/members/import-export/import/importer.ts
          - core/server/data/db/connection.ts
          - core/server/data/schema/schema.js
        tests:
          - test/e2e-api/admin/member-custom-fields.test.ts
          - test/e2e-api/members/custom-fields.test.ts
          - test/e2e-api/admin/members-import-custom-fields.test.ts
          - test/unit/server/services/members/import-export/import/error-handling.test.ts
      failures:
        summary: >-
          Invalid plans reject before writes; malformed value leaves are logged/omitted and
          malformed history field lists become empty, while invalid definition decoding rejects
          the read. Bindings skip unresolved/invalid rows and return ordered good plans plus the
          first failure. Definition action failures are logged after data writes. Multi-property definition edits are not atomic; limit
          counting deliberately permits concurrent overshoot, and applying a plan does not
          revalidate its captured definition. HTTP asserts selected refusals/corrupt reads and
          successful actions; fake binding/import tests assert continuation/reporting.
          Action failure, partial edits, stale plans and concurrency guarantees remain untested.
        sources:
          - core/server/services/members-metafields/definitions-service.ts
          - core/server/services/members-metafields/values-service.ts
          - core/server/services/members-metafields/bindings-service.ts
          - core/server/services/members-metafields/actions.ts
          - core/server/services/members/import-export/import/importer.ts
        tests:
          - test/e2e-api/admin/member-custom-fields.test.ts
          - test/e2e-api/members/custom-fields.test.ts
          - test/unit/server/services/members-metafields/bindings-service.test.ts
          - test/unit/server/services/members/import-export/import/error-handling.test.ts
    unverified: []
    blockers:
      - >-
        Pin the real root exports, pre-init behavior, synchronous publication, repeated-init
        identities and partial-construction retry. Cover retained members/CSV consumers,
        including shared database replacement/restart;
        constructor units and successful HTTP boot do not establish those lifecycle contracts.
      - >-
        Add real database failure assertions for member/value/history writes after
        work has begun, and for post-write action-log failure. Preserve the narrower existing
        definition-batch rollback evidence; prevalidation refusals and fake transaction tests
        do not establish these outcomes or event delivery relative to commit/rollback.
      - >-
        Characterize concurrent definition edits/deletion, deliberately approximate limit
        enforcement, and plans applied after definition access/status changes. Multi-property
        edits can retain earlier writes after later failure, and feed rows/count use separate
        reads; no atomicity or snapshot guarantee should be introduced by the migration.
      - >-
        Characterize binding resolution against real archived/malformed rows before retaining
        or changing that API during migration. Existing binding units use fake query/value
        collaborators; they do not establish database behavior or in-flight work settlement.
        Confirm ownership of the constructed bindings service now that the inspected
        production consumers were removed.
  mentions:
    kind: directory
    disposition: migrate
    status: audited
    migrated: true
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: complex
      modelImpact: fits
      reason: >-
        The boot-owned defineService root constructs the repository, metadata, discovery,
        resource, routing, API and controller objects, awaits the controller wiring with the
        jobs instance, builds the sender with per-call URL, privacy and post-URL callbacks,
        registers its six listeners as the last step and returns one facade. initServices
        awaits it after Newsletters and before the group containing Recommendations, which
        reads the ready facade; jobs registration retains the ready controller and sender.
      reviewedAt: 8cba94a138a69cbb7e496424591575daab11a529
      sources:
        - core/server/services/mentions/index.ts
        - core/server/services/mentions/mention-controller.js
        - core/server/services/mentions/mention-sending-service.js
        - core/server/services/recommendations/index.ts
        - core/boot.js
      dependencies:
        complexity: complex
        reason: >-
          The root injects logging into the repository, controller, discovery, sender, API and
          routing objects and the oEmbed service into metadata, keeps models, events, the request
          client, URL helpers, the serializer URL helper, settings and DomainEvents in index.ts,
          and constructs the post-URL helpers with the URL service and serializer. Recommendations
          reads the same ready metadata object and adds its mapper; registered jobs retain the
          controller and sender. Three implementation files require cheerio/slim lazily for HTML
          parsing under its exact allowance.
        preparation: []
        reviewedAt: d90bf15bb1aa9b7c47ef2ff40e7d65b688ec22d3
        sources:
          - core/server/services/mentions/index.ts
          - core/server/services/mentions/post-urls.ts
          - core/server/services/mentions/webmention-metadata.js
          - core/server/services/mentions/mention-sending-service.js
          - core/server/services/recommendations/index.ts
          - core/server/services/jobs-service/register-job-handlers.ts
          - core/boot.js
    entryPoints:
      - mentions/index.ts
    notes: >-
      Migration inspected 8cba94a138a69cbb7e496424591575daab11a529 with the characterization from the
      preceding tests commit. Boot owns the facade; the Admin endpoint, job registration and
      Recommendations read the ready service. Logging and the oEmbed service are injected and
      post-URL resolution is a helper built from the URL service and serializer; cheerio/slim
      stays imported where used under its exact allowance. Listener/mapper ownership and
      transaction gaps remain.
    contracts:
      exports:
        summary: >-
          The TypeScript root default-exports a defineService initializer whose service is a
          facade of api, repository, controller, metadata and sendingService; reading it before
          init throws. There are no extras. Post-URL helpers are an implementation factory, not
          a root export. Job registration deep-imports ProcessWebmentionJob and
          SendWebmentionsJob; recommendations deep-imports MentionCreatedEvent. Root units
          assert the pre-init throw and the facade published once init resolves. Job units
          assert type strings and manual JSON reconstruction, not backend persistence.
        sources:
          - core/server/services/mentions/index.ts
          - core/server/services/mentions/post-urls.ts
          - core/server/services/mentions/process-webmention-job.ts
          - core/server/services/mentions/send-webmentions-job.ts
          - core/server/services/mentions/mention-created-event.js
          - core/server/services/jobs-service/register-job-handlers.ts
          - core/server/services/recommendations/index.ts
        tests:
          - test/unit/server/services/mentions/index.test.ts
          - test/unit/server/services/mentions/process-webmention-job.test.ts
          - test/unit/server/services/mentions/send-webmentions-job.test.ts
      construction:
        summary: >-
          The create callback requires models, events, the request client, URL helpers, the
          serializer URL helper, settings, DomainEvents, logging and the oEmbed root (which
          acquires its image-storage adapter and providers), reads the jobs instance, then
          constructs the Bookshelf repository, metadata, discovery, resource, routing (site
          URL captured here), API and controller with all its collaborators, builds the sender
          with per-call callbacks and the post-URL helpers, registers listeners last and
          returns the facade. Root units drive the real wiring:
          receive dispatches ProcessWebmentionJob through the jobs service given to init,
          browse resolves resources through models.Post (null ids skipped, title and type
          mapped, resourceId removed), and processWebmention creates a mention through
          models.Mention with the resolved post resource, oEmbed metadata, verification
          against the fetched body and a MentionCreatedEvent dispatch. Metadata units
          assert the oEmbed call (type mention, 15-second timeout, retry limit three),
          the field mapping with null image/favicon, recursive mapper application and
          the separate verification fetch of the original page when a mapper changed the
          URL. Direct implementation construction and InMemoryMentionRepository occur in
          tests, not another runtime root.
        sources:
          - core/server/services/mentions/index.ts
          - core/server/services/mentions/post-urls.ts
          - core/server/services/mentions/mention-controller.js
          - core/server/services/mentions/mention-sending-service.js
          - core/server/services/mentions/bookshelf-mention-repository.js
          - core/server/services/mentions/webmention-metadata.js
          - core/server/services/mentions/in-memory-mention-repository.js
          - core/server/services/oembed/service.js
        tests:
          - test/unit/server/services/mentions/index.test.ts
          - test/unit/server/services/mentions/webmention-metadata.test.js
      consumers:
        summary: >-
          Source-checked: Admin browse and public webmention receive retain the root and
          read the ready service's controller per call. Browse adds a source exclusion, normalizes
          options and maps mentions/resources to the HTTP shape; receive awaits job
          dispatch and returns 202 without awaiting processing. Boot passes concrete
          controller/sender objects to job handlers on the webmentions queue with
          concurrency three. Recommendations retains api/sender for refresh/list/sendAll,
          adds a metadata mapper and subscribes to MentionCreatedEvent. Real HTTP tests
          assert a limited Admin browse snapshot, incoming persisted metadata/payload,
          verification and deletion, and outgoing post/page publish/edit/unpublish
          requests against Nock. The receive route uses a 64kb form-body limit; a real HTTP
          assertion expects 413 for an oversized body, not exact-boundary acceptance coverage.
          Direct-dispatch integrations assert saved incoming
          fields and outgoing source/target form fields. Recommendation email tests
          save a real mention through the ready repository then use test event settlement
          and mocked mail delivery.
        sources:
          - core/boot.js
          - core/server/services/mentions/index.ts
          - core/server/api/endpoints/mentions.js
          - core/server/api/endpoints/utils/serializers/input/mentions.js
          - core/server/api/endpoints/utils/serializers/output/mappers/mentions.js
          - core/server/web/api/endpoints/admin/routes.js
          - core/server/web/webmentions/routes.js
          - core/server/services/mentions/mention-controller.js
          - core/server/services/jobs-service/register-job-handlers.ts
          - core/server/services/recommendations/index.ts
          - core/server/services/recommendations/service/incoming-recommendation-service.ts
          - core/server/services/recommendations/service/recommendation-service.ts
        tests:
          - test/e2e-api/admin/mentions.test.js
          - test/e2e-api/webmentions/webmentions.test.js
          - test/e2e-server/services/mentions.test.js
          - test/integration/services/mentions/process-webmention-job.test.ts
          - test/integration/services/mentions/send-webmentions-job.test.ts
          - test/e2e-server/services/recommendation-emails.test.js
          - test/unit/server/services/jobs-service/register-job-handlers.test.ts
      startup:
        summary: >-
          Boot awaits mentions after Newsletters and before the Promise.all group that
          initializes Recommendations, and before registering and starting jobs. VM boot
          units hold its initializer unresolved and assert that Recommendations is not
          initialized and registration/start/automations/rescheduling stay pending;
          rejection propagates the identical error without those dependent calls. Other
          services are fakes. The facade is published only when init resolves; root units
          assert the pre-init throw. Real HTTP tests exercise successful boot composition,
          not held or failing construction of the real root.
        sources:
          - core/boot.js
          - core/server/services/mentions/index.ts
          - core/server/services/mentions/mention-controller.js
          - core/server/services/recommendations/index.ts
        tests:
          - test/unit/boot/init-services.test.ts
          - test/unit/server/services/mentions/index.test.ts
          - test/e2e-api/webmentions/webmentions.test.js
          - test/e2e-server/services/mentions.test.js
      lifetime:
        summary: >-
          Source-checked: defineService retains the first instance, so later init calls
          return without replacing dependencies; there is no root reset/shutdown. Controller retains API/jobs/resource lookup,
          sender retains jobs/request/discovery/callbacks, API retains repository and
          helpers, and metadata retains all added mappers. Registered jobs and
          recommendations retain concrete objects if wrapper fields are later replaced.
          A jobs unit constructs a real controller with a fake API and real in-memory
          backend, retains it across jobs shutdown/restart, and asserts stable jobs
          identity plus exact first/second delivery to the current handler. Root units
          assert that repeated init retains api, repository, sendingService, controller and
          metadata, with exactly one owned listener per event. Nothing exercises sender
          restart or listener cleanup.
        sources:
          - core/server/services/mentions/index.ts
          - core/server/services/mentions/mention-controller.js
          - core/server/services/mentions/mention-sending-service.js
          - core/server/services/mentions/mentions-api.js
          - core/server/services/mentions/webmention-metadata.js
          - core/server/services/jobs-service/index.ts
          - core/server/services/jobs-service/register-job-handlers.ts
          - core/server/services/recommendations/index.ts
        tests:
          - test/unit/server/services/jobs-service/retained-consumer.test.ts
          - test/unit/server/services/mentions/index.test.ts
      configuration:
        summary: >-
          Source-checked: routing captures a site URL at init. Sender calls URL helpers
          per use, but the shared URL helper freezes site/subdirectory/admin URLs at
          import; this is not a URL hot-reload contract. Privacy is read from settings
          cache before enqueueing post events, not rechecked by sendWebmentions/sendAll.
          Source URL and changed links are captured before dispatch. Helper
          units assert missing-relation loading and post/page serializer selection;
          sender units assert fixed enablement gates, captured payload fields and
          destroyed-post previous data. Discovery/send/metadata request 15-second
          timeouts and retry limit three; shared transport test hooks replace these
          with five seconds/no retries. The installed Got defaults exclude POST retries; setting
          that limit alone does not enable them. Routing and transport units cover selected
          URL/status/form cases, not production retries or root configuration changes.
          Discovery resolves relative Link-header and HTML endpoints against the response URL;
          units assert the relative Link case and HTML resolution after a redirect. Root
          units assert, through the registered listener and the real callbacks, that
          privacy is read from the settings cache when the post event fires, that the
          site URL used to filter links is read per use, that the dispatched job carries
          the resolved source URL and changed links, and that routing compares targets
          against the site URL captured at init.
        sources:
          - core/server/services/mentions/index.ts
          - core/server/services/mentions/post-urls.ts
          - core/server/services/mentions/routing-service.js
          - core/server/services/mentions/resource-service.js
          - core/server/services/mentions/mention-sending-service.js
          - core/server/services/mentions/mention-discovery-service.js
          - core/server/services/mentions/webmention-metadata.js
          - core/shared/url-utils.ts
          - core/server/lib/request-external.js
        tests:
          - test/unit/server/services/mentions/index.test.ts
          - test/unit/server/services/mentions/service.test.js
          - test/unit/server/services/mentions/mention-sending-service.test.js
          - test/unit/server/services/mentions/routing-service.test.ts
          - test/unit/server/services/mentions/resource-service.test.ts
          - test/unit/server/services/mentions/mention-discovery-service.test.ts
      resources:
        summary: >-
          Source-checked: six raw model-event listeners start asynchronous preparation
          that the emitter does not await. There is no unsubscribe, mapper removal,
          producer drain or request cancellation on this root. Jobs own queued execution;
          shared models own database access and request-external owns keep-alive agents.
          oEmbed retains provider/storage dependencies; mention fetches skip image
          persistence but can probe favicon URLs, including a stream destroyed after
          its first chunk. Repository save awaits the model write then dispatches entity
          events without awaiting subscribers or clearing events. DomainEvents catches
          subscriber errors; its normal allSettled tracking is enabled only in tests.
          Registration units assert queue options; HTTP sender tests explicitly wrap
          raw listeners and job completion. Root units assert six owned listeners named
          bound sendForPost (the name the HTTP tests wrap) and that a failed construction
          registers none. Listening is the last step of create, so a failed attempt has
          nothing to unwind. These are test observation/drain mechanisms, not evidence of
          production shutdown or provider resource disposal.
        sources:
          - core/server/services/mentions/mention-sending-service.js
          - core/server/services/mentions/webmention-metadata.js
          - core/server/services/mentions/bookshelf-mention-repository.js
          - core/server/services/mentions/mention.js
          - core/server/lib/common/events.js
          - core/server/models/base/plugins/events.js
          - core/server/services/jobs-service/register-job-handlers.ts
          - core/server/lib/request-external.js
          - core/server/services/oembed/service.js
          - core/server/services/oembed/oembed-service.js
          - core/server/services/recommendations/index.ts
          - package.json
        tests:
          - test/unit/server/services/jobs-service/register-job-handlers.test.ts
          - test/unit/server/services/mentions/index.test.ts
          - test/e2e-server/services/mentions.test.js
          - test/e2e-server/services/recommendation-emails.test.js
      failures:
        summary: >-
          Root units pin construction failure: init rejects, the service stays unavailable,
          no listeners are registered, and an explicit later init constructs again.
          The synchronous create has no awaited step. Receive dispatch errors
          propagate, whereas controller processing and sender event-preparation errors
          are logged/swallowed. Discovery fetch and parser failures warn and return null;
          individual POST failures are logged and later targets continue. Discovery units
          assert null for an invalid Link endpoint, warning rather than error logging for a
          failed fetch, and null without a warning when content type is missing. A dispatched-job
          integration asserts that an invalid earlier endpoint does not prevent delivery to a
          later linked page; it does not establish completion timing for the whole job.
          Routing HEAD failures become false. API rejects invalid new mentions, retains
          existing mentions on transient metadata failure, deletes after three hard
          failures and resets the count after success. Controller units assert swallowed
          API/URL errors; sender Nock tests assert selected HTTP/network rejection and
          continuation after one failed send. Metadata units inject 429/503/timeout/404
          errors and tag a failed verification page fetch by transient classification;
          API units assert survival/deletion/restoration through an in-memory
          repository, not persisted counters. Real HTTP covers target deletion and
          selected verification outcomes, not construction failure or atomic DB/event work.
        sources:
          - core/server/services/mentions/index.ts
          - core/server/services/mentions/mention-controller.js
          - core/server/services/mentions/mention-sending-service.js
          - core/server/services/mentions/mention-discovery-service.js
          - core/server/services/mentions/routing-service.js
          - core/server/services/mentions/mentions-api.js
          - core/server/services/mentions/webmention-metadata.js
          - core/server/services/mentions/bookshelf-mention-repository.js
          - core/server/services/mentions/mention.js
        tests:
          - test/unit/server/services/mentions/index.test.ts
          - test/unit/server/services/mentions/mention-controller.test.ts
          - test/unit/server/services/mentions/mention-sending-service.test.js
          - test/unit/server/services/mentions/mention-discovery-service.test.ts
          - test/unit/server/services/mentions/webmention-metadata.test.js
          - test/unit/server/services/mentions/mentions-api.test.js
          - test/e2e-api/webmentions/webmentions.test.js
          - test/integration/services/mentions/send-webmentions-job.test.ts
    unverified: []
    blockers:
      - >-
        Cover listener/mapper ownership and overlapping work before introducing reset or
        shutdown. Raw event preparation and recommendation sendAll calls run outside the
        jobs backend's drain; queued processing can finish before domain subscribers.
        Root units pin one owned listener per event across repeated init and metadata
        units pin recursive mapper application, but repeated addMapper calls still
        accumulate, an unconditional mapper recurses until the stack overflows, and
        retained-controller jobs coverage does not cover the sender through a jobs restart.
        HTTP tests supply their own observation/settlement mechanisms.
      - >-
        Root units characterize configuration timing through the real wrapper: current
        privacy at event time, the site URL read per use for link filtering, routing's
        init snapshot, and the source URL/changed links captured for jobs. They stub the
        shared URL helpers and settings cache, so frozen shared URLs, production hot
        reload and privacy changes while jobs are pending remain unproven.
      - >-
        Before changing repository or transport composition, add focused assertions for
        persisted revalidation counts and write/event failure ordering.
        Model writes and event delivery are not one transaction; lookup/create is not
        atomic and re-saving a new entity can redispatch its uncleared events. API units
        share in-memory entities and do not prove SQL uniqueness/filtering or persistence.
        Nock/test hooks do not establish production retry, timeout, cancellation or socket
        cleanup. These source-derived gaps are scoped to mentions, not pilot prerequisites.
  milestones:
    kind: directory
    disposition: migrate
    status: audited
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: complex
      modelImpact: fits
      reason: >-
        One retained API fits defineService, but initAndRun also creates a new background
        timer on every call. Separate construction from scheduling and decide timer
        ownership and failure policy: a rejected run currently leaves scheduleRun's outer
        promise pending. Boot must keep milestone checks outside foreground readiness,
        without accidentally making successful init imply check completion.
      reviewedAt: f276513ba32b7114fd5cc4f7b02d3f82c0c31e3e
      sources:
        - core/server/services/milestones/service.js
        - core/server/services/milestones/milestones-service.js
        - core/boot.js
      dependencies:
        complexity: straightforward
        reason: >-
          Repository, queries and milestone logic already accept their dependencies. The external
          imports are confined to service.js, which acts as the legacy composition root. Moving that
          wiring to index.ts and keeping Stripe eligibility as a live settings callback is bounded;
          the timer and run failure policy are lifecycle work rather than dependency-graph
          obstacles.
        preparation:
          - >-
            Move models, database, event bus, logger and configuration wiring from service.js into
            index.ts.
          - >-
            Retain a call-time Stripe eligibility callback and the existing captured milestone
            settings when separating the runner from scheduling.
        reviewedAt: 44ad405bff4a8700af2ed51f833ebc08d25dddff
        sources:
          - core/server/services/milestones/service.js
          - core/server/services/milestones/milestones-service.js
          - core/server/services/milestones/bookshelf-milestone-repository.js
          - core/server/services/milestones/milestone-queries.js
    entryPoints:
      - milestones/index.js
    notes: >-
      Preserve background startup, captured milestone configuration and live Stripe eligibility.
      Scheduling, failure and cleanup gaps remain migration blockers.
      Slack notifications live in slack-notifications/ as the private milestone notification
      handler: they subscribe only to MilestoneCreatedEvent and read milestones host settings.
      First Milestones init starts them before any check can create a milestone, so boot no
      longer initializes them separately; migrating Milestones must also own their disposal.
      Their previous contract audit is recorded at
      https://github.com/TryGhost/Ghost/blob/33b8dabf2114676d68028d6f86c1945af4e7409c/ghost/core/server/services/service-inventory.yaml#L12917.
    contracts:
      exports:
        summary: >-
          CommonJS index forwards the cached service object, exposing mutable api, init, run,
          scheduleRun and initAndRun. Methods use this; api starts null and becomes a
          MilestonesService with checkMilestones. Runtime event consumers deep-import the
          MilestoneCreatedEvent class rather than the wrapper.
        sources:
          - core/server/services/milestones/index.js
          - core/server/services/milestones/service.js
          - core/server/services/milestones/milestones-service.js
          - core/server/services/milestones/milestone-created-event.js
        tests:
          - test/unit/server/services/milestones/index.test.ts
      construction:
        summary: >-
          Import loads models, repository and event dependencies and samples JOB_TIMEOUT.
          First init synchronously constructs BookshelfMilestoneRepository, MilestoneQueries
          and MilestonesService before its async method resolves; later calls retain api.
          Queries retain the shared db module and the repository retains models.Milestone and
          DomainEvents. InMemoryMilestoneRepository and direct implementation construction
          are used by tests, not another runtime composition path.
        sources:
          - core/server/services/milestones/service.js
          - core/server/services/milestones/milestone-queries.js
          - core/server/services/milestones/bookshelf-milestone-repository.js
          - core/server/services/milestones/in-memory-milestone-repository.js
        tests:
          - test/e2e-server/services/milestones.test.js
      consumers:
        summary: >-
          Background boot is the only runtime wrapper caller and invokes initAndRun. Staff
          and slack-notifications subscribe to the deep-imported milestone event for email
          and webhook delivery. Milestone creation records events; repository save dispatches
          them after its database write without awaiting consumers. The root has no HTTP or
          CLI caller and does not send mail itself.
        sources:
          - core/boot.js
          - core/server/services/milestones/milestone.js
          - core/server/services/milestones/bookshelf-milestone-repository.js
          - core/server/services/staff/staff-service.js
          - core/server/services/staff/staff-service-emails.js
          - core/server/services/milestones/slack-notifications/slack-notifications-service.js
        tests:
          - test/unit/server/services/milestones/milestone.test.ts
          - test/unit/server/services/staff/index.test.js
          - test/unit/server/services/milestones/slack-notifications/index.test.js
      startup:
        summary: >-
          Boot starts background services after mounting the app and notifying readiness,
          skips their main sequence in test environments, and does not await its completion.
          Milestones are started at the end of that sequence after earlier awaited services;
          initAndRun is itself unawaited. Direct run needs api initialized, whereas initAndRun
          initializes it before scheduling. Initialization performs composition, not a
          milestone query or readiness check. First init also starts the private Slack
          notification subscriber once, before any check can dispatch a milestone event.
        sources:
          - core/boot.js
          - core/server/services/milestones/service.js
          - core/server/services/milestones/slack-notifications/service.js
        tests:
          - test/unit/boot/init-background-services.test.ts
          - test/unit/server/services/milestones/index.test.ts
          - test/e2e-server/services/milestones.test.js
      lifetime:
        summary: >-
          The wrapper and api survive repeated init and server stop; no reset or shutdown is
          exposed. Each scheduleRun or initAndRun creates another independent one-shot timer,
          with no recurring registration, deduplication or overlap guard. run checks members
          before eligible ARR and reads persisted milestones to avoid recreating an already
          recorded goal during serial runs. Repository events remain on the saved entity.
        sources:
          - core/server/services/milestones/service.js
          - core/server/services/milestones/milestones-service.js
          - core/server/services/milestones/bookshelf-milestone-repository.js
          - core/server/ghost-server.ts
        tests:
          - test/e2e-server/services/milestones.test.js
          - test/unit/server/services/milestones/milestones-service.test.js
      configuration:
        summary: >-
          First init retains the milestones config object and captures
          minDaysSinceImported, using 7 for a falsy value. Each run reads current Stripe keys
          after the members check; development accepts pk_test_ and other environments
          accept pk_live_. scheduleRun rereads NODE_ENV, forces 5000 ms in development and
          otherwise uses customTimeout or the import-time random delay of 0, 1, 2 or 3 days.
          Zero customTimeout selects that fallback. Database contents and the clock are read
          when checks execute.
        sources:
          - core/server/services/milestones/service.js
          - core/server/services/milestones/milestones-service.js
          - core/server/services/milestones/milestone-queries.js
          - core/shared/config/validated.ts
          - core/shared/config/defaults.json
        tests:
          - test/e2e-server/services/milestones.test.js
          - test/unit/server/services/milestones/milestones-service.test.js
      resources:
        summary: >-
          The service owns raw timeouts but stores no handle, does not unref them and has no
          cancellation or drain hook. Boot does not register milestone cleanup with
          GhostServer; jobs-service shutdown does not own these timers. Queries borrow the
          shared db module and the repository borrows the model layer; neither opens a
          dedicated connection or starts an encompassing transaction. Event subscriptions
          and delivery resources belong to staff and slack-notifications, and run completion
          does not await their delivery.
        sources:
          - core/server/services/milestones/service.js
          - core/server/services/milestones/milestone-queries.js
          - core/server/services/milestones/bookshelf-milestone-repository.js
          - core/server/data/db/index.js
          - core/boot.js
          - core/server/ghost-server.ts
          - core/server/services/staff/staff-service.js
          - core/server/services/milestones/slack-notifications/slack-notifications-service.js
        tests: []
      failures:
        summary: >-
          Construction errors reject init before api assignment, allowing another attempt.
          Direct run rejects on query, validation or save failure and does not proceed to ARR
          after members failure. scheduleRun awaits run in an async timer callback but never
          rejects its outer promise, so a run rejection leaves callers pending and produces
          an unhandled rejection; boot installs a logging handler. Earlier persisted goals
          are not rolled back as a group. Staff and Slack handlers catch and log notification
          failures after the milestone write, without making run await delivery or retry it.
        sources:
          - core/server/services/milestones/service.js
          - core/server/services/milestones/milestones-service.js
          - core/server/services/milestones/bookshelf-milestone-repository.js
          - core/boot.js
          - core/server/services/staff/staff-service.js
          - core/server/services/milestones/slack-notifications/slack-notifications-service.js
        tests:
          - test/unit/server/services/milestones/slack-notifications/slack-notifications-service.test.js
    unverified: []
    blockers:
      - >-
        The wrapper unit test only checks that initAndRun exists. Add exact export and identity
        coverage, init failure/retry, use before init and retained consumers across restart.
        Existing E2E coverage calls initAndRun directly; it does not establish the real boot
        readiness boundary or behavior when earlier background initialization fails.
      - >-
        Characterize the existing timer rejection and pending outer promise in an isolated
        process, then decide failure policy separately. No cancellation, drain, restart or
        overlapping-schedule contract is tested; each initAndRun currently adds a timer.
      - >-
        Add configuration timing coverage for captured milestone settings, import-age fallback,
        import-time random delay, explicit zero timeout and the development overrides. Current
        tests do not establish these boundaries or Stripe changes during a pending run.
      - >-
        Characterize overlapping runs and partial persistence before choosing a concurrency
        contract. Goal lookup and saves are not one transaction, the table has no unique
        goal constraint, and saving the same entity again redispatches its retained events.
        Persistence precedes notification completion; email_sent_at is not a delivery receipt.
      - >-
        Verify production repository behavior with multiple prior emailed goals. Bookshelf
        getLastEmailSent selects email_sent_at ASC, while the fake makes value descending primary and email date
        only breaks equal-value ties. The production repository and query unit tests only assert
        method presence, and the E2E test does not distinguish these ordering rules.
      - >-
        Add a production repository-to-event-to-notification contract. Staff/Slack tests
        dispatch events manually, implementation tests use the fake repository and E2E
        does not assert delivery; removing Bookshelf event dispatch could escape those tests.
      - >-
        Slack notifications: Add wrapper contracts for exact exports, repeated init, changed
        configuration, disabled first init, partial subscription failure/retry and retained
        callbacks across restart. The existing root test proves one successful event reaches a
        webhook, not listener count, configuration lifetime or boot readiness.
      - >-
        Slack notifications: Correct the threshold test: it sets isEnabled false and omits
        minThreshold, so its no-send assertion does not establish threshold behavior. Cover
        equality, values either side of the threshold, wrapper fallback and invalid parsing
        through real composition.
      - >-
        Slack notifications: Characterize sender/request failures through the real subscriber,
        including malformed events, timeouts and pending sends during stop. Tests named Throws for
        invalid or absent URLs only assert logging after a resolved call; preserve that behavior
        unless changing it separately. Test-mode request overrides do not prove production
        transport behavior.
      - >-
        Slack notifications: Decide listener removal, pending-send drain and shared HTTP-agent
        ownership. Current class tests create subscribers directly; successful callbacks and fake
        requests do not establish disposal, duplicate delivery prevention or restart behavior.
  newsletters:
    kind: directory
    disposition: migrate
    status: audited
    migrated: true
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: straightforward
      modelImpact: fits
      reason: >-
        The boot-owned defineService root constructs the token provider and the existing
        implementation with the shared models, mail module, URL facade, limits, labs, the
        email-address wrapper, logging and Sentry. initServices awaits it after Email Address,
        Member Attribution and Audience Feedback and before the group containing Members, which
        reads the ready instance while composing its API. The implementation keeps constructing
        GhostMailer and MagicLink; no extras or different instance lifetime are needed.
      reviewedAt: 0780d0a76e340fd2d131e782a164633c4d389084
      sources:
        - core/server/services/newsletters/index.ts
        - core/server/services/newsletters/newsletters-service.js
        - core/server/services/members/api.js
        - core/boot.js
      dependencies:
        complexity: straightforward
        reason: >-
          The root already injects models, token provider, URL, limits, labs and the Email Address
          facade. The implementation still constructs GhostMailer and MagicLink and imports
          logging/debug/Sentry/lodash, but these fit its existing constructor without a reciprocal
          startup dependency. Members captures the newsletter instance, and its token provider is a
          reusable constructor rather than a dependency on initialized Members.
        preparation:
          - >-
            Move mailer/MagicLink construction and remaining library/logger wiring into index.ts
            while retaining the separate token-provider instance and per-send callbacks.
          - >-
            Initialize after Email Address and before Members captures Newsletters; preserve live
            policy/settings reads rather than caching their values during construction.
        reviewedAt: 44ad405bff4a8700af2ed51f833ebc08d25dddff
        sources:
          - core/server/services/newsletters/index.ts
          - core/server/services/newsletters/newsletters-service.js
          - core/server/services/members/single-use-token-provider.js
          - core/server/services/members/api.js
          - core/boot.js
    entryPoints:
      - newsletters/index.ts
    notes: >-
      Migration inspected 0780d0a76e340fd2d131e782a164633c4d389084 with the verification
      characterization from the preceding tests bundle. Boot owns the instance; Members and the
      Admin endpoint read the ready service. Logging and Sentry are injected, lodash is replaced
      with plain copies, and the magic-link constructor is an exact-file allowance, so mailer and
      MagicLink construction stays in the implementation rather than moving to the root as the
      earlier preparation note proposed. auditedAt and the dependency assessment retain
      historical provenance. Mail transport ownership/drain and retained-consumer lifetime
      through reboot remain outside this bounded change.
    contracts:
      exports:
        summary: >-
          The TypeScript root default-exports defineService with init and guarded service and
          no extras. service is the retained mutable NewslettersService; its unbound async
          read/edit/add/verifyPropertyUpdate methods return models, browse returns the model's
          page result, and getAll returns a JSON array. Constructor dependencies, ghostMailer
          and magicLinkService remain accessible properties; internal methods are only
          documented private. The root unit checks four truthy API properties on the ready
          service, not their type, complete surface or identity. Class tests pin read/model
          option forwarding, add/edit metadata and verification-property allowlisting with fakes.
        sources:
          - core/server/services/newsletters/index.ts
          - core/server/services/newsletters/newsletters-service.js
        tests:
          - test/unit/server/services/newsletters/index.test.ts
          - test/unit/server/services/newsletters/service.test.js
      construction:
        summary: >-
          The root factory constructs a SingleUseTokenProvider and NewslettersService at init
          with the shared models, mail module, URL facade, limits, labs, email-address wrapper,
          logging and Sentry; the implementation constructor still creates GhostMailer and
          MagicLink. The captured labs object is unused by current methods. Mail
          configuration/transport failure now rejects init rather than import; nothing is
          acquired, so there is no cleanup. Members requires the root at import and reads its
          service inside createApiInstance; the Admin endpoint reads the service per query.
          Class units construct injected instances; root units initialize the real root and
          drive the actual instance, MagicLink and token provider with stubbed model and mail
          I/O. Constructor failure and retained graph identity remain source-only.
        sources:
          - core/boot.js
          - core/server/services/newsletters/index.ts
          - core/server/services/newsletters/newsletters-service.js
          - core/server/lib/mail/ghost-mailer.js
          - core/server/services/members/service.js
          - core/server/services/members/api.js
          - core/server/api/endpoints/index.js
          - core/server/api/endpoints/newsletters.js
        tests:
          - test/unit/server/services/newsletters/index.test.ts
      startup:
        summary: >-
          initServices initializes email-address, then awaits Member Attribution, Audience
          Feedback and this root before the group containing members.init, so Members composes
          with the ready instance. The guarded getter rejects earlier reads under the kernel
          contract; importing the endpoint or the Members API module reads no service. The
          constructor retains the email-address wrapper without reading its service; address
          validation and sending access emailAddressService.service later and have no
          readiness guard. Models/database, settings used by mail, and limits must be ready
          when operations run. There is no explicit newsletter start or stop step. Successful
          booted HTTP cases exercise usable composition; the initServices harness answers this
          root with a generic initializer, so no focused test proves its ordering, early-call
          failure or stop/reboot behavior.
        sources:
          - core/boot.js
          - core/server/services/newsletters/index.ts
          - core/server/services/newsletters/newsletters-service.js
          - core/server/services/members/service.js
          - core/server/lib/mail/ghost-mailer.js
        tests:
          - test/e2e-api/admin/newsletters.test.js
      consumers:
        summary: >-
          Admin controllers retain the root and call methods on its service as receivers.
          Members composition reads the ready service when its API is created and retains it
          in both MemberRepository (default active, auto-opt-in,
          members-visible subscriptions) and RouterController (requested name lookup and
          active/archive validation). Welcome emails deep-imports only the verification
          template. The Content API reads Newsletter models directly, so its tests are
          not newsletter-root coverage. Admin HTTP assertions cover browse/read/add/edit,
          relation counts, duplicate names, limit errors, address policy and intercepted
          verification messages; the default-member HTTP case pins subscribed IDs and
          resulting events. Router unit assertions use a getAll fake, not real query/root
          composition. Root units issue a link through the mailer and apply its token through
          verifyPropertyUpdate to the named newsletter property; real HTTP completion and
          retained-consumer lifetime remain gaps.
        sources:
          - core/server/api/endpoints/newsletters.js
          - core/server/api/endpoints/newsletters-public.js
          - core/server/services/members/api.js
          - core/server/services/members/members-api/members-api.js
          - core/server/services/members/members-api/repositories/member-repository.js
          - core/server/services/members/members-api/controllers/router-controller.js
          - core/server/services/member-welcome-emails/index.ts
        tests:
          - test/e2e-api/admin/newsletters.test.js
          - test/e2e-api/admin/members.test.js
          - test/unit/server/services/members/members-api/controllers/router-controller.test.js
          - test/unit/server/services/newsletters/index.test.ts
      lifetime:
        summary: >-
          The root retains one instance after successful init; repeated init does not replace
          it, and there is no reset/replacement/disposal API.
          Add deep-clones attributes, overwrites sort_order, and mutates caller options
          with a new transaction only when opt_in_existing is true and none is supplied;
          edit forwards a supplied transaction but does not open its own. Neither limit
          check nor next-sort lookup serializes competing creations. Verification sends
          run sequentially per operation, but all operations share a MagicLink whose
          transporter is replaced before awaiting token creation. Units pin fake option
          forwarding; HTTP pins six persisted opt-in relations. These do not prove
          concurrency isolation, failure rollback or retained graph identity.
        sources:
          - core/server/services/newsletters/index.ts
          - core/server/services/newsletters/newsletters-service.js
          - core/server/models/newsletter.js
          - core/server/lib/magic-link/magic-link.js
        tests:
          - test/unit/server/services/newsletters/service.test.js
          - test/e2e-api/admin/newsletters.test.js
      configuration:
        summary: >-
          Root token limits are fixed at 24 hours total, ten minutes after first use and
          seven uses; they are not true single-use tokens. Mail transport/options are
          captured at construction, while address policy is read through the wrapper per call.
          Verification omits from; GhostMailer delegates default sender selection to the
          email-address policy at send time and reads settings for names/tracking.
          URL generation calls a retained facade whose site/admin bases are frozen.
          Changes requiring verification are withheld, then metadata names sent/verified
          fields; special newsletter/support reply-to values bypass address validation.
          Verification applies token data without rerunning current address policy.
          HTTP tests pin managed/custom-domain/self-hosted edit policies and messages
          using test config replacement, not production hot reload. Root units pin the undefined
          sender passed to the mailer and, with a fake clock and token model, the configured
          24-hour lifetime, ten-minute after-use limit and seventh/eighth-use boundary.
        sources:
          - core/server/services/newsletters/index.ts
          - core/server/services/newsletters/newsletters-service.js
          - core/server/services/email-address/index.js
          - core/server/services/email-address/email-address-service.ts
          - core/server/services/members/single-use-token-provider.js
          - core/server/lib/mail/ghost-mailer.js
          - core/shared/url-utils.ts
          - core/shared/config/index.ts
        tests:
          - test/e2e-api/admin/newsletters.test.js
          - test/unit/server/services/members/single-use-token-provider.test.js
          - test/unit/server/services/newsletters/index.test.ts
      resources:
        summary: >-
          The root owns a GhostMailer transport and MagicLink/token-provider objects, but
          no listeners, timers, queue or response cache directly. Selected mail transports
          own their network/process resources; the root neither closes them nor tracks
          pending sends. Members jobs delete shared tokens older than 24 hours; newsletters
          owns neither cleanup scheduling nor deletion. Token creation writes independently of
          newsletter transactions. Validation opens its own transaction and locks/updates
          token usage before returning data; the later newsletter update is separate.
          Injected token tests assert lock/save options and usage/expiry calculations; root
          units assert lock options and root-specific limits through the actual provider.
          Stubbed model transactions do not establish real locking or cleanup. HTTP mail is
          intercepted.
        sources:
          - core/server/services/newsletters/newsletters-service.js
          - core/server/services/members/single-use-token-provider.js
          - core/server/lib/magic-link/magic-link.js
          - core/server/lib/mail/ghost-mailer.js
          - core/server/models/single-use-token.js
          - core/server/services/members/jobs/index.js
          - core/server/services/members/jobs/clean-tokens-task.ts
        tests:
          - test/unit/server/services/members/single-use-token-provider.test.js
          - test/unit/server/services/newsletters/index.test.ts
      failures:
        summary: >-
          Mail transport construction failure rejects init and leaves the service unavailable.
          Read maps a missing model to NotFound; limit and ordinary model errors propagate;
          coded errors mentioning unique become the duplicate-name validation error.
          Add/edit persist before verification messages, and opt-in creation sends before
          its transaction finishes. Token creation, message generation/sending, reread or
          a later verification send can fail after earlier work; token validation consumes
          usage before property validation or model update. Creating a custom reply-to
          requiring verification dereferences an absent newsletter in the preparation
          method. Units pin selected propagation/allowlist behavior with fakes, and HTTP
          pins duplicate/limit/address errors. Root units assert that a failed token write sends
          no mail, mail rejection propagates and a property-update rejection follows recorded
          token usage; other partial failures remain uncovered. The edit missing-data unit now
          calls edit with valid options and asserts no model edit; the HTTP active-edit-at-limit
          case selects a non-active row. API serialization separately masks invalid old
          sender addresses; that presentation does not establish a stored correction.
        sources:
          - core/server/services/newsletters/newsletters-service.js
          - core/server/services/members/single-use-token-provider.js
          - core/server/lib/magic-link/magic-link.js
          - core/server/api/endpoints/utils/serializers/output/mappers/newsletters.js
        tests:
          - test/unit/server/services/newsletters/service.test.js
          - test/e2e-api/admin/newsletters.test.js
          - test/unit/server/services/newsletters/index.test.ts
    unverified: []
    blockers:
      - >-
        Boot now owns construction, but the initServices harness does not pin this root's
        ordering. Still protect the exact public surface and retained Admin/member consumers
        through stop/reboot and dependency failure, and define ownership/drain of the mail
        transport.
      - >-
        Root units now cover issuance through the mailer to token verification, the configured
        expiry/reuse limits and property-update rejection after token usage. Still cover the
        real HTTP issuance-to-verification chain, wrong-flow consumption, actual persistence
        and locking failure, the mailer default-sender fallback and policy changes between
        issuance and use; stubbed models do not establish those contracts.
      - >-
        Protect opt-in rollback and ordinary add/edit persistence when reread, token writes
        or first/later mail sends fail. Establish concurrent limit/sort-order/opt-in behavior
        and the shared MagicLink transporter boundary; do not infer atomicity from success.
      - >-
        Characterize new-newsletter custom reply-to verification and genuine active editing at
        the host limit. Missing edit input is now tested through edit with valid options; keep
        other legacy failures explicit rather than changing them during migration.
  notifications:
    kind: directory
    disposition: migrate
    status: audited
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: complex
      modelImpact: fits
      reason: >-
        The cached Notifications instance fits the helper, but its capability boundary is
        split: the API persists transformed arrays, reads can start detached repair writes,
        and update-check constructs a separate email helper. Settle which operations belong
        on the ready notification facade and which helper stays caller-owned; preserve
        persistence/error timing while moving construction behind boot.
      reviewedAt: f276513ba32b7114fd5cc4f7b02d3f82c0c31e3e
      sources:
        - core/server/services/notifications/index.js
        - core/server/services/notifications/notifications.js
        - core/server/api/endpoints/notifications.js
        - core/server/services/notifications/notification-email.ts
        - core/server/services/update-check/index.js
      dependencies:
        complexity: straightforward
        reason: >-
          The retained notifications instance already receives its cache and Settings model.
          Remaining dependencies are version comparison, utility and HTML-sanitizing functions, plus
          legacy Moment. The separately constructed email helper uses the same sanitizer, so its
          caller must retain an explicit composition path; no service cycle is needed for these
          dependencies.
        preparation:
          - >-
            Inject version/comparison, utility and sanitizing operations, and use the existing
            kernel clock for Moment.
          - >-
            Give the caller-constructed email helper an explicit sanitizer dependency or a shared
            library home; update Update Check without making it depend on notification-service
            readiness.
        reviewedAt: 44ad405bff4a8700af2ed51f833ebc08d25dddff
        sources:
          - core/server/services/notifications/index.js
          - core/server/services/notifications/notifications.js
          - core/server/services/notifications/notification-email.ts
          - core/server/services/notifications/sanitize-notification-html.ts
          - core/server/services/update-check/index.js
          - core/server/api/endpoints/notifications.js
    entryPoints:
      - notifications/index.js
      - notifications/notification-email.ts
    notes: >-
      Preserve the nested notifications export and per-user visibility contracts. Notification
      persistence is split with the API controller; settle its business-logic owner and the
      separately constructed email helper's ownership before migration.
    contracts:
      exports:
        summary: >-
          CommonJS index exports an object with one notifications instance, without init or
          shutdown. Its add method returns existing and new notification arrays; browse and
          destroyAll return arrays. destroy returns an array, a rejected promise, or a promise
          resolving undefined for an already-seen item. The implementation exports its
          constructor. The instance also exposes fetchAllNotifications, areNotificationsValid,
          wasSeen, dangerousDestroyAll and mutable dependency fields. Deep TypeScript exports
          provide NotificationEmailService and the HTML
          sanitizer. These do not share the index instance.
        sources:
          - core/server/services/notifications/index.js
          - core/server/services/notifications/notifications.js
          - core/server/services/notifications/notification-email.ts
          - core/server/services/notifications/sanitize-notification-html.ts
        tests: []
      construction:
        summary: >-
          Index import constructs Notifications with the shared settings cache and Settings
          model. The only other constructor calls found are tests. Each permitted update-check
          invocation independently constructs NotificationEmailService with a new GhostMailer,
          the shared template generator and a live site-URL callback. The notification API
          controller also eagerly constructs a SettingsBREADService, whose constructor creates
          another mailer and magic-link helper; those are not index-owned dependencies.
        sources:
          - core/server/services/notifications/index.js
          - core/server/services/notifications/notifications.js
          - core/server/services/notifications/notification-email.ts
          - core/server/services/update-check/index.js
          - core/server/api/endpoints/notifications.js
          - core/server/services/settings/settings-service.js
          - core/server/services/settings/settings-bread-service.js
          - core/server/models/base/listeners.js
          - core/server/models/index.js
        tests: []
      consumers:
        summary: >-
          The notification API controller is the only runtime index consumer found. Admin
          routes expose browse/add/dismiss through authenticated API pipelines; destroyAll is
          an internal endpoint without an HTTP route. Update-check awaits notification adds
          through the API, while the mail endpoint starts an unawaited warning add after direct
          transport failure. Update-check is the only runtime notification-email constructor
          consumer. HTTP serialization removes internal seen/version/time fields from responses.
        sources:
          - core/server/api/endpoints/notifications.js
          - core/server/web/api/endpoints/admin/routes.js
          - core/server/api/endpoints/utils/serializers/output/notifications.js
          - core/server/api/endpoints/mail.js
          - core/server/services/update-check/index.js
          - core/server/services/update-check/update-check-service.js
        tests:
          - test/e2e-api/admin/notifications.test.js
          - test/integration/jobs/update-check.test.ts
      startup:
        summary: >-
          This root has no boot hook or readiness guard. Construction stores dependencies;
          operations need a populated settings cache, and persistence needs the Settings model.
          Boot initializes database and settings before mounting the API. Reading before cache
          initialization is treated as corrupt notification data and starts an asynchronous
          reset rather than returning a readiness error. NotificationEmailService needs its
          injected mail/template dependencies ready; update-check invokes it from initialized
          jobs, after resolving recipients.
        sources:
          - core/boot.js
          - core/server/services/notifications/notifications.js
          - core/shared/settings-cache/cache-manager.js
          - core/server/services/notifications/notification-email.ts
          - core/server/services/update-check/update-check-service.js
          - core/server/services/jobs-service/register-job-handlers.ts
        tests: []
      lifetime:
        summary: >-
          The module-cached instance retains dependency references, not a private notification
          list. Operations reread the notifications setting; normal stored JSON is parsed into
          a fresh array by settings cache. add/destroy/destroyAll transform that data, while the
          API controller persists the complete resulting array. Dismissal retains records and
          adds the user to seenBy; legacy seen applies only without seenBy. Cache reinitialization
          preserves the cache-manager object, so the retained root reads its current store.
          A model listener prunes old seen records except explicit release notifications,
          using seen rather than seenBy. No notification-level serialization protects the
          concurrent read/replace operations.
        sources:
          - core/server/services/notifications/index.js
          - core/server/services/notifications/notifications.js
          - core/server/api/endpoints/notifications.js
          - core/shared/settings-cache/index.js
          - core/shared/settings-cache/cache-manager.js
          - core/server/models/settings.js
          - core/server/services/settings/settings-bread-service.js
          - core/server/models/base/listeners.js
          - core/server/models/index.js
        tests:
          - test/legacy/models/base/listeners.test.js
          - test/e2e-api/admin/notifications.test.js
          - test/unit/server/services/notifications/notifications.test.js
      configuration:
        summary: >-
          The index reads no config snapshot. Each operation reads current cached notification
          data; add reads the current Ghost version and time for defaults, while browse applies
          version/seen filters and sorts by addedAt. Sanitization uses a fixed semantic-tag and
          URL-scheme allowlist on add, browse and email rendering. The email helper captures
          dependencies, resolves its URL callback once per nonempty send, then renders and
          sends separately for each recipient; update-check resolves recipient addresses.
        sources:
          - core/server/services/notifications/notifications.js
          - core/server/services/notifications/sanitize-notification-html.ts
          - core/server/services/notifications/notification-email.ts
          - core/server/services/update-check/update-check-service.js
          - core/server/lib/mail/index.js
          - core/server/lib/mail/email-content-generator.js
        tests:
          - test/unit/server/services/notifications/notifications.test.js
          - test/unit/server/services/notifications/sanitize-notification-html.test.ts
          - test/unit/server/services/notifications/notification-email.test.ts
          - test/unit/server/services/update-check.test.js
      resources:
        summary: >-
          The Notifications instance borrows settings/model resources and creates no timer, listener, pool
          or queue. Normal API mutations await SettingsBREADService persistence; malformed
          non-array data instead triggers an untracked SettingsModel.edit reset from the read
          path. Model import registers a settings.notifications.edited listener which can
          start another whole-array settings write without the original edit awaiting it.
          NotificationEmailService borrows its injected mailer and template generator,
          awaits sequential sends and owns no cleanup. The per-check mail transport belongs to
          update-check's composition; the API's eager SettingsBREADService has separate mail
          resources. Neither path exposes notification-specific cancellation or drain.
        sources:
          - core/server/services/notifications/notifications.js
          - core/server/services/notifications/notification-email.ts
          - core/server/api/endpoints/notifications.js
          - core/server/services/settings/settings-bread-service.js
          - core/server/services/update-check/index.js
          - core/server/lib/mail/ghost-mailer.js
          - core/server/models/base/listeners.js
          - core/server/models/index.js
        tests:
          - test/unit/server/services/notifications/notifications.test.js
          - test/unit/server/services/notifications/notification-email.test.ts
      failures:
        summary: >-
          add throws ValidationError for a non-string new message. destroy rejects with
          NoPermissionError for an undismissible item and NotFoundError for an unknown ID.
          Only the outer stored array is validated; invalid members or version data can throw.
          A non-array read returns empty immediately while resetting storage; reset rejection
          is not caught or returned to that caller. The separate age-pruning listener calls
          errors.logError on write rejection, but that method is absent in the current errors
          package, replacing the write failure with TypeError.
          API write failures propagate. Email render,
          URL or send failures reject and stop later recipients after any earlier deliveries;
          update-check decides whether to continue notification storage or rethrow.
        sources:
          - package.json
          - core/server/services/notifications/notifications.js
          - core/server/services/notifications/notification-email.ts
          - core/server/api/endpoints/notifications.js
          - core/server/services/update-check/update-check-service.js
          - core/server/models/base/listeners.js
          - core/server/models/index.js
        tests:
          - test/unit/server/services/notifications/notifications.test.js
    unverified: []
    blockers:
      - >-
        Move persistence orchestration to its business owner only after characterizing the
        current root/API split. API add persists only when notificationsToAdd is nonempty;
        destroy always calls settings edit, including when the root returns undefined for an
        already-seen item. Keep HTTP permissions, statuses and serialization at the API layer.
      - >-
        Add export, construction, pre-init and same-process restart contracts for the retained
        root and both mail composition paths. Current constructor tests use fake caches; API
        tests cover HTTP behavior but not root identity, dependency rebinding or mail disposal.
      - >-
        Characterize concurrent add/dismiss/repair and failed persistence with real storage.
        Whole-array replacement has no notification-level lock or version check, so overlapping
        snapshots can lose additions or seenBy updates. Existing multi-user tests are sequential.
      - >-
        Decide and test read-path repair ownership. Invalid data and pre-init reads start a
        detached reset that may repeat, race a valid write, reject after the caller returns or
        outlive shutdown. The existing test proves only an empty result and a resolving stub
        call with the string '[]', not repair completion, internal context, failure or durability.
      - >-
        Add batch/deduplication, release replacement, ordering and dismissal edge contracts.
        Defaults generate one ID per add call, and duplicates are checked only against existing
        rows; missing IDs or duplicate IDs within a batch are not characterized. destroyAll
        sets seen without updating seenBy. Missing/undismissible/already-seen errors and malformed
        array entries/version values lack focused coverage.
      - >-
        Characterize notification-email partial delivery, live URL/template settings and render
        or send rejection. Helper tests prove successful recipient rendering, sanitization and
        empty-recipient behavior; successful job integration does not prove transport cleanup,
        failure propagation or shutdown draining. Decide the helper's owner separately from
        the cached notification list.
      - >-
        Include model-owned age pruning in concurrency, failure and shutdown contracts. It
        starts a separate whole-array write and ignores seenBy, so one user's dismissal can
        make an old custom notification eligible for global deletion. Legacy listener tests
        check counts after success, including a fixed delay; they do not cover races, drain
        or the unavailable errors.logError rejection handler.
  oembed:
    kind: directory
    disposition: migrate
    status: audited
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: straightforward
      modelImpact: fits
      reason: >-
        The root composes one shared implementation, image adapter and two ordered providers
        without starting background work. Construct them after adapter setup and before
        Mentions/Recommendations use the service, then update direct instance consumers.
        Preserve NFT-before-Twitter registration, captured credentials and image-adapter
        identity.
      reviewedAt: f276513ba32b7114fd5cc4f7b02d3f82c0c31e3e
      sources:
        - core/server/services/oembed/service.js
        - core/server/services/oembed/oembed-service.js
        - core/server/services/mentions/webmention-metadata.js
        - core/server/services/recommendations/index.ts
        - core/boot.js
      dependencies:
        complexity: complex
        reason: >-
          Transport, config and image storage are injected, but provider discovery, image processing
          and standalone metadata extractors still load their own runtime libraries. Under the
          current small allowlist, establishing DI spans the metadata/provider helper graph as well
          as moving import-time adapter acquisition. That is substantial local composition work,
          without evidence that the initializer model itself must change.
        preparation:
          - >-
            Introduce explicit local metadata/provider helper factories so parser, extraction,
            encoding and image operations are wired at the root; retain SSRF-protected transport on
            every fetching path.
          - >-
            Preserve deferred loading of heavy provider/metadata helpers and distinguish startup
            provider credentials from live config reads.
          - >-
            Move image-adapter acquisition to boot-owned construction before Mentions and
            Recommendations consume oEmbed; review any proposed pure-library allowance individually.
        reviewedAt: 44ad405bff4a8700af2ed51f833ebc08d25dddff
        sources:
          - core/server/services/oembed/service.js
          - core/server/services/oembed/oembed-service.js
          - core/server/services/oembed/extract-metadata.ts
          - core/server/services/oembed/extract-oembed.ts
          - core/server/services/oembed/twitter-oembed-provider.js
          - core/server/services/mentions/webmention-metadata.js
    entryPoints:
      - oembed/index.js
    notes: >-
      Preserve the shared instance, custom-provider precedence, metadata consumers and image
      side effects. Credentials and the image adapter are captured at import; request-time
      configuration and caller options have different scopes.
    contracts:
      exports:
        summary: >-
          CommonJS index forwards an eager OEmbedService instance; deep modules export its
          constructor and provider constructors. Fetch/scrape/image operations return promises
          and use receiver dependencies. Config, request client, imageStore and customProviders are mutable
          properties; synchronous registerProvider appends without validation or deduplication.
          Custom providers run before card-type dispatch and the first result other than null
          wins. Mention results skip thumbnail storage. Class tests pin selected metadata,
          provider lookup, YouTube rewriting, image behavior and custom-provider mention handling.
          Root units pin the exported OEmbedService instance, operations that reject without it
          as their receiver, and each registered provider's result returned unchanged for every
          card type.
        sources:
          - core/server/services/oembed/index.js
          - core/server/services/oembed/service.js
          - core/server/services/oembed/oembed-service.js
          - core/server/services/oembed/nft-oembed-provider.js
          - core/server/services/oembed/twitter-oembed-provider.js
        tests:
          - test/unit/server/services/oembed/oembed-service.test.js
          - test/unit/server/services/oembed/index.test.ts
      construction:
        summary: >-
          The service module is the only production constructor site found. Import obtains
          storage:images from the shared adapter manager, constructs OEmbedService, then registers
          NFT followed by Twitter with copied credentials. Construction stores dependencies;
          provider HTTP and image writes happen on calls. Dependency imports create shared request
          agents, and adapter lookup may construct a configured storage client. Deep-class tests
          inject dependencies. Root units pin adapter acquisition during require, the shared
          config, request client and images adapter, NFT-then-Twitter registration, a missing
          provider config subtree or adapter error rejecting the import with nothing cached, and
          a later import composing afresh.
        sources:
          - core/server/services/oembed/service.js
          - core/server/services/oembed/oembed-service.js
          - core/kernel/adapter-manager/adapter-manager.ts
          - core/server/adapters/storage/LocalImagesStorage.ts
          - core/server/adapters/storage/S3Storage.ts
          - core/server/lib/request-external.js
        tests:
          - test/unit/server/services/oembed/oembed-service.test.js
          - test/unit/server/services/oembed/index.test.ts
          - test/e2e-api/admin/oembed.test.js
      consumers:
        summary: >-
          The authenticated Admin oembed endpoint retains the instance and serializes image URLs.
          WebmentionMetadata retains it for mention metadata/body and optional original-page
          verification; recommendations injects it into a retained metadata service that calls it
          as fallback after its separate Ghost-site probe. Only composition registers providers
          in production searches.
          Admin HTTP assertions pin response/error shapes, URL mapping, charset handling and
          blocked destinations with mocked HTTP/DNS; most successful image processing is stubbed.
          Webmention tests stub root fetching to assert transient-error tagging. Recommendation
          tests inject a fake oembed service and pin fallback mapping, not the real root's mention
          payload or timeout propagation. Custom-provider results need not contain their expected
          metadata/body; WebmentionMetadata dereferences metadata without a guard. Root units call
          the Admin endpoint, real WebmentionMetadata and real recommendation fallback against
          the shared instance as receiver. For a custom provider result WebmentionMetadata throws
          a TypeError, pinned as current behaviour, while recommendations falls back to null
          fields; a JSON mention page yields null metadata with its body and content type.
        sources:
          - core/server/api/endpoints/oembed.js
          - core/server/api/endpoints/utils/validators/input/oembed.js
          - core/server/api/endpoints/utils/serializers/output/oembed.js
          - core/server/api/endpoints/utils/serializers/output/mappers/oembed.js
          - core/server/web/api/endpoints/admin/routes.js
          - core/server/services/mentions/webmention-metadata.js
          - core/server/services/recommendations/index.ts
          - core/server/services/recommendations/service/recommendation-metadata-service.ts
        tests:
          - test/e2e-api/admin/oembed.test.js
          - test/unit/server/services/mentions/webmention-metadata.test.js
          - test/unit/server/services/recommendations/service/recommendation-metadata-service.test.ts
          - test/unit/server/services/oembed/index.test.ts
      startup:
        summary: >-
          There is no init or readiness gate: importing consumers performs composition, and calls
          can immediately fetch or write. Mentions requires WebmentionMetadata and the oEmbed
          service inside its defineService factory; recommendations requires oembed during init. Boot validates adapters in initCore before
          initServices but has no explicit oembed startup/drain registration. Missing provider
          config subtrees or adapter construction errors can reject the importing boot path.
          The Admin suite boots Ghost and exercises requests. Root units pin composition while
          the module is required, import failure leaving no cached root, and repeated imports
          returning one instance without composing again.
        sources:
          - core/boot.js
          - core/server/services/oembed/service.js
          - core/server/services/mentions/index.ts
          - core/server/services/mentions/webmention-metadata.js
          - core/server/services/recommendations/index.ts
        tests:
          - test/unit/server/services/oembed/index.test.ts
          - test/e2e-api/admin/oembed.test.js
      lifetime:
        summary: >-
          Module caching retains the same instance/provider array and captured adapter/client.
          There is no root result cache, in-flight deduplication, reset or shutdown. Adapter-cache
          clearing does not replace this retained imageStore. Each scrape parses the page afresh;
          concurrent calls share providers/dependencies. Bookmark page fetch and optional
          enrichment run together and allSettled waits for both; enrichment has its own abort
          deadline. Image writes get fresh UUID names rather than reuse existing files. Units
          assert distinct keys for repeated identical bytes and fallback after slow enrichment.
          Root units pin repeated imports returning one instance and image writes using the
          import-time adapter after the adapter manager returns another; concurrent registration
          and retained callers after replacement remain untested.
        sources:
          - core/server/services/oembed/oembed-service.js
          - core/server/services/oembed/service.js
          - core/kernel/adapter-manager/adapter-manager.ts
          - core/server/services/mentions/webmention-metadata.js
          - core/server/services/recommendations/service/recommendation-metadata-service.ts
        tests:
          - test/unit/server/services/oembed/oembed-service.test.js
          - test/unit/server/services/oembed/index.test.ts
      configuration:
        summary: >-
          Provider credentials are copied at import, while image allowlists are read per
          operation. Request-external captures agent sizing but reads env/site URL for
          destination checks; development and same-site exemptions apply. Test config replacement
          is not production hot reload. Page calls accept fetch options; custom/ordinary known
          providers, image downloads, favicon probes/HEAD and discovered oembed JSON do not inherit
          that whole option object. Recommendations does not pass its timeout to oembed fallback.
          Test-mode request hooks force zero retries and a 5s request timeout; production keeps
          dependency defaults unless overridden. Units pin selected timeout arguments, enrichment
          abort/fallback and configurable image extensions; they do not prove an overall deadline
          or production retry policy. Root units pin credentials copied at import: a later
          config value is not sent. Provider units pin the NFT asset request with its optional
          key header, and the Twitter API request, query fields, bearer header, tweet_data and
          x.com rewrite, with the provider endpoint fetched through the injected client.
        sources:
          - core/server/services/oembed/service.js
          - core/server/services/oembed/oembed-service.js
          - core/server/services/oembed/extract-oembed.ts
          - core/server/services/oembed/twitter-oembed-provider.js
          - core/server/services/oembed/nft-oembed-provider.js
          - core/server/services/recommendations/service/recommendation-metadata-service.ts
          - core/server/lib/request-external.js
          - core/shared/config/validated.ts
          - package.json
        tests:
          - test/unit/server/services/oembed/oembed-service.test.js
          - test/unit/server/services/oembed/twitter-embed.test.js
          - test/unit/server/services/oembed/nft-oembed.test.ts
          - test/unit/server/services/oembed/index.test.ts
      resources:
        summary: >-
          This root owns no subscription or recurring timer. Calls borrow shared HTTP/HTTPS agents,
          allocate response/image buffers and favicon streams, and create enrichment abort timers.
          Favicon probing requests a byte range and destroys the stream on first data; other image
          downloads buffer the whole response before SVG limits/content checks. SVG conversion
          invokes native image processing with a 10s processing timeout; storage saveRaw writes a
          UUID path through the captured adapter, including filesystem writes or an S3 PutObject.
          No root cancellation, operation drain or saved-file compensation exists. Units exercise
          real conversion with fake storage and selected real local HTTP probes; the four-byte
          probe fixture does not prove early cancellation of a large/chunked response. HTTP tests
          restore real image fetching for private-address and nonimage cases, not successful
          persistence through real storage. Nested client/native cleanup remains uncharacterized.
        sources:
          - core/server/services/oembed/oembed-service.js
          - core/server/lib/request-external.js
          - core/server/adapters/storage/LocalStorageBase.ts
          - core/server/adapters/storage/S3Storage.ts
          - core/kernel/adapter-manager/adapter-manager.ts
          - core/boot.js
          - package.json
        tests:
          - test/unit/server/services/oembed/oembed-service.test.js
          - test/e2e-api/admin/oembed.test.js
      failures:
        summary: >-
          Private dispatch catches awaited failures, consults shouldRethrowFetchError, preserves
          ValidationErrors, otherwise logs and substitutes unknown-provider validation. Returned
          knownProvider/explicit-bookmark promises can reject outside that catch; knownProvider
          maps extractor errors itself. Enrichment failure is optional; scraper failure becomes
          unknown-provider and absent title is insufficient-metadata. Icon/image failures log and
          retain documented fallback URLs; optional Twitter enrichment failure logs but preserves
          its base embed. No rollback removes earlier saved files. Units pin selected rethrows,
          invalid schema/images and fallback behavior; several known-provider rejection tests use
          catch-only assertions. HTTP assertions strongly pin selected 422/error and private-address
          failures. Root units pin a mention page without a content type becoming an
          unknown-provider rejection with the TypeError logged, and a plain-text mention page
          failing for insufficient metadata. NFT units pin API failure propagation; its parsed
          response assumption remains a source risk without a reproduction.
        sources:
          - core/server/services/oembed/oembed-service.js
          - core/server/services/oembed/extract-oembed.ts
          - core/server/services/oembed/oembed-schema.ts
          - core/server/services/oembed/nft-oembed-provider.js
          - core/server/services/oembed/twitter-oembed-provider.js
          - core/server/services/mentions/webmention-metadata.js
        tests:
          - test/unit/server/services/oembed/oembed-service.test.js
          - test/unit/server/services/oembed/twitter-embed.test.js
          - test/unit/server/services/oembed/index.test.ts
          - test/unit/server/services/oembed/nft-oembed.test.ts
          - test/e2e-api/admin/oembed.test.js
    unverified: []
    blockers:
      - >-
        Custom providers bypass mention and bookmark dispatch, so WebmentionMetadata throws on
        their results while recommendation fallback reads null fields. Root units pin this as
        current behaviour; whether mention requests should skip custom providers is undecided.
      - >-
        NFT calls Got json() then reads result.body, although the installed client's shortcut
        returns the parsed body. Units pin its request and failure propagation, not a
        successful payload.
      - >-
        Pin branch-specific option forwarding, rethrow classification, retries and cancellation
        across page/enrichment, known/custom providers, discovery, probes, images and storage.
        Include returned-promise catch boundaries, caller timeout loss in recommendation fallback,
        production request hooks and large/chunked probe cancellation; test mode changes transport
        defaults. Strengthen catch-only rejection assertions before treating those cases as guards.
      - >-
        Characterize successful root-to-adapter writes, adapter changes with retained callers,
        partial persistence and shutdown with active streams/native work/shared clients. Existing
        fake saveRaw assertions prove names and calls, not filesystem/S3 completion or cleanup.
        Preserve content-based image validation, SVG conversion and private-destination guards
        without assuming the HTTP fixtures cover every dependency resource or DNS failure.
  offers:
    kind: directory
    disposition: migrate
    status: audited
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: complex
      modelImpact: fits
      reason: >-
        Initialization publishes the API before awaiting initial redirect loading and
        installs event listeners first; re-init replaces the graph while frontend middleware
        and Members retain older objects. Build one API/redirect facade, await loading, and
        give failed attempts exact subscription cleanup. The existing helper fits once
        publication and retained-consumer behavior are made explicit.
      reviewedAt: f276513ba32b7114fd5cc4f7b02d3f82c0c31e3e
      sources:
        - core/server/services/offers/service.js
        - core/server/services/offers/offers-module.js
        - core/frontend/web/site.js
        - core/server/services/members/api.js
        - core/boot.js
      dependencies:
        complexity: complex
        reason: >-
          Root composition is small, but external dependencies remain in repository/API/module
          layers and in static domain value creation: OfferCode calls slugify and the shared
          ValueObject imports lodash equality. Injecting only the repository and event bus will not
          satisfy the rule. Frontend redirects and Members capture the resulting objects, so helper
          preparation must preserve entity/event identity and the completed API/redirect graph.
        preparation:
          - >-
            Inject DomainEvents, reporting/logging and repository query transforms from index.ts,
            preserving event publication after transaction completion.
          - >-
            Give slug normalization and value comparison an explicit construction seam, or replace
            their narrow uses with characterized local behavior; avoid introducing a module-global
            dependency setter for static factories.
          - >-
            Publish one API/redirect graph after its initial load and adapt retained
            frontend/Members consumers; subscription cleanup belongs in the separate lifecycle work.
        reviewedAt: 44ad405bff4a8700af2ed51f833ebc08d25dddff
        sources:
          - core/server/services/offers/service.js
          - core/server/services/offers/offers-module.js
          - core/server/services/offers/offer-bookshelf-repository.js
          - core/server/services/offers/application/offers-api.js
          - core/server/services/offers/domain/models/offer-code.js
          - core/server/services/offers/domain/models/shared/value-object.js
          - core/frontend/web/site.js
          - core/server/services/members/api.js
    entryPoints:
      - offers/index.js
    notes: >-
      Preserve offers API, frontend redirects and retained members consumers. Repeated init
      rebuilds composition and adds domain-event listeners; redirect publication and coupon
      side effects are separate from database transaction completion.
    contracts:
      exports:
        summary: >-
          The CommonJS index forwards one mutable object with receiver-dependent async init,
          initially null api and a middleware getter. Before init the getter throws through
          undefined redirectManager; afterward it returns that manager's bound handler. API
          methods are async and receiver-dependent, returning DTOs, arrays or null for absent
          offers. Deep modules expose constructible implementation/value classes and event
          identities. There is no reset/shutdown/readiness API. Root units pin the shape and the
          pre-init throw; consumer units pin per-request endpoint reads with their receivers.
        sources:
          - core/server/services/offers/index.js
          - core/server/services/offers/service.js
          - core/server/services/offers/offers-module.js
          - core/server/services/offers/application/offers-api.js
          - core/server/services/offers/application/offer-mapper.ts
          - core/server/web/shared/dynamic-redirect-manager.js
        tests:
          - test/unit/server/services/offers/index.test.ts
          - test/unit/server/services/offers/consumers.test.ts
          - test/unit/server/services/offers/application/offers-api.test.js
          - test/unit/server/services/offers/domain/models/offer.test.js
      construction:
        summary: >-
          Import loads shared config, URL helpers and models without constructing a redirect
          graph. Every init creates a DynamicRedirectManager, model-backed repository and
          OffersModule/API; it publishes redirectManager and this.api before awaiting module
          init. That module first subscribes to two offer events, then loads existing offers
          and adds temporary redirects, including archived offers. Boot is the only production
          root init caller found. No idempotency guard, shared pending promise or rollback
          protects concurrent/repeated construction. Root units pin publication before the load,
          subscription order, the unfiltered load and rebuilding on each init.
        sources:
          - core/server/services/offers/service.js
          - core/server/services/offers/offers-module.js
          - core/server/services/offers/offer-bookshelf-repository.js
          - core/boot.js
        tests:
          - test/unit/server/services/offers/index.test.ts
      consumers:
        summary: >-
          Admin and Content endpoints retain the root and dereference api per request; frontend
          mounting captures the current bound middleware. Members passes the current API to its
          BREAD, payment, repository and router collaborators for offer projection, checkout,
          availability, applying discounts and mapping imported Stripe coupons. Payments also
          subscribes to the deep OfferCreatedEvent and can create a Stripe coupon afterward.
          Admin/Content HTTP tests pin create/read/edit/archive, validation and projection; members
          tests cover availability
          and discount rules with Stripe mocked. The bulk-redemption integration constructs its
          own API. Units pin per-request endpoint reads, the site app reading the middleware once
          at setup, Members receiving the API value at construction, and offer-event-to-redirect
          wiring through a real redirect manager over faked models. A booted frontend test pins
          startup rows, archived offers, query strings and Admin API create/code-change redirects
          through the mounted site app and the database.
        sources:
          - core/server/api/endpoints/offers.js
          - core/server/api/endpoints/offers-public.js
          - core/frontend/web/site.js
          - core/server/services/members/api.js
          - core/server/services/members/members-api/members-api.js
          - core/server/services/members/members-api/services/member-bread-service.js
          - core/server/services/members/members-api/services/payments-service.js
          - core/server/services/members/members-api/repositories/member-repository.js
          - core/server/services/members/members-api/controllers/router-controller.js
        tests:
          - test/unit/server/services/offers/consumers.test.ts
          - test/unit/server/services/offers/index.test.ts
          - test/e2e-frontend/offer-redirects.test.ts
          - test/e2e-api/admin/offers.test.js
          - test/e2e-api/content/offers.test.js
          - test/e2e-api/members/member-offers.test.js
          - test/integration/services/offers-api.test.js
      startup:
        summary: >-
          Boot awaits offers init after core initialization and themes, before Express frontend
          mounting, members composition and readiness. This runs even with frontend disabled.
          The exported API can already be used while initial repository loading is pending;
          external callers receive no readiness guard. Boot propagates init rejection, while
          already-published API/managers/subscriptions remain. Units pin the frontend phase
          awaiting offers after themes, the API published during a pending load and a rejected
          load leaving the API and subscriptions in place.
        sources:
          - core/boot.js
          - core/server/services/offers/service.js
          - core/server/services/offers/offers-module.js
          - core/frontend/web/site.js
        tests:
          - test/unit/boot/init-services-for-frontend.test.ts
          - test/unit/server/services/offers/index.test.ts
          - test/e2e-api/admin/offers.test.js
          - test/e2e-api/content/offers.test.js
      lifetime:
        summary: >-
          Re-init replaces published API/manager, but mounted middleware and members collaborators
          retain earlier objects. Each old OffersModule remains reachable through its event
          listeners, so later events update old and new redirect maps, as root units pin.
          Rows/redemptions persist
          independently; API reads query afresh with no root cache or operation serialization.
          Code-change events remove the old redirect and add the new one; archiving has no
          redirect-removal event. Transaction-scoped uniqueness checks and DB unique name/code/
          coupon indexes differ from concurrent retention-offer policy, which has no unique
          cadence constraint. Sequential HTTP retention tests do not establish concurrency.
        sources:
          - core/server/services/offers/service.js
          - core/server/services/offers/offers-module.js
          - core/server/services/offers/application/offers-api.js
          - core/server/services/offers/offer-bookshelf-repository.js
          - core/server/services/offers/domain/models/offer.js
          - core/server/data/schema/schema.js
          - core/frontend/web/site.js
          - core/server/services/members/api.js
        tests:
          - test/unit/server/services/offers/index.test.ts
          - test/e2e-api/admin/offers.test.js
      configuration:
        summary: >-
          init captures custom-redirect permanent max-age; current offer redirects explicitly
          use permanent false, producing 302 with max-age zero. The subdirectory callback uses
          shared URL helpers per request. Offer rules come from data/value objects rather than
          retained config subtrees: signup requires a tier, retention forbids one, codes slugify,
          and only selected fields can update. Repository filtering allows status/redemption_type
          and maps status to active; availability additionally filters tier/cadence/type/redemptions.
          Unit and HTTP tests cover these policies and DTO boundaries. Real repository tests
          pin status/type mapping, redemption counts and last-redeemed values, not same-instance root
          reconfiguration. Units pin the 302/max-age zero response, query strings, the per-request
          subdirectory and the status/redemption_type transform. Config replacement in tests is
          not production hot reload.
        sources:
          - core/server/services/offers/service.js
          - core/server/web/shared/dynamic-redirect-manager.js
          - core/server/services/offers/domain/models/offer.js
          - core/server/services/offers/domain/models/offer-code.js
          - core/server/services/offers/application/offers-api.js
          - core/server/services/offers/application/offer-mapper.ts
          - core/server/services/offers/offer-bookshelf-repository.js
          - core/shared/config/validated.ts
        tests:
          - test/unit/server/services/offers/index.test.ts
          - test/unit/server/services/offers/offer-bookshelf-repository.test.ts
          - test/unit/server/services/offers/domain/models/offer.test.js
          - test/unit/server/services/offers/application/offers-api.test.js
          - test/e2e-api/admin/offers.test.js
          - test/e2e-api/members/member-offers.test.js
          - test/integration/services/offer-bookshelf-repository.test.js
      resources:
        summary: >-
          Each graph owns redirects and two shared DomainEvents subscriptions, borrowing models
          and database transactions. create/update/list start their own transaction; getOffer
          and ensureOfferForStripeCoupon reuse a supplied transaction. Repository save attaches
          event dispatch to transaction.executionPromise without awaiting it; a rollback dispatches
          nothing and its rejection is handled there, leaving the error to the caller;
          nontransactional save dispatches immediately. Subscriber work, including payment-owned
          Stripe calls and coupon-ID persistence, is not awaited by API success. There is no
          unsubscribe or pending-work drain. A real-database API test pins created-event dispatch
          after commit, and a rollback after the save with no dispatch, no stored row and no
          unhandled rejection; a repository unit pins the missing dispatch and rejection. The
          members redemption case proves a row exists inside its real transaction, not provider
          rollback.
        sources:
          - core/server/services/offers/offers-module.js
          - core/server/services/offers/offer-bookshelf-repository.js
          - core/server/services/offers/application/offers-api.js
          - core/server/services/members/members-api/services/payments-service.js
          - core/server/services/members/members-api/repositories/member-repository.js
          - core/server/ghost-server.ts
          - package.json
        tests:
          - test/unit/server/services/offers/offer-bookshelf-repository.test.ts
          - test/integration/services/offers-api.test.js
          - test/unit/server/services/offers/application/offers-api.test.js
          - test/e2e-api/members/member-offers.test.js
      failures:
        summary: >-
          Repository model/query failures normally propagate, while domain reconstruction errors
          log/report and yield null, so lists/startup omit malformed offers and reads can appear
          absent. A real repository test pins omission/logging of a signup offer with a missing
          product; units add its Sentry report, the direct-read null and its omission at startup,
          not every malformed row. Count/last-redemption queries
          run outside that catch. API validation rejects;
          HTTP read/edit translates absent results to 404. Coupon import catches duplicate-key
          save errors, rereads the coupon offer and returns it or throws a logged InternalServerError;
          other save failures escape. Unit fakes pin these branches, not a real database race.
          Redirect callback failures are caught by DomainEvents; failed startup does not undo
          listeners/maps, as root units pin. A rolled-back save dispatches nothing, as the
          real-database API test pins. Post-commit failures remain source-only; successful HTTP
          create is not proof of coupon provisioning completion.
        sources:
          - core/server/services/offers/service.js
          - core/server/services/offers/offers-module.js
          - core/server/services/offers/offer-bookshelf-repository.js
          - core/server/services/offers/application/offers-api.js
          - core/server/api/endpoints/offers.js
          - core/server/api/endpoints/offers-public.js
          - core/server/services/members/members-api/services/payments-service.js
          - package.json
        tests:
          - test/unit/server/services/offers/index.test.ts
          - test/unit/server/services/offers/offer-bookshelf-repository.test.ts
          - test/unit/server/services/offers/application/offers-api.test.js
          - test/e2e-api/admin/offers.test.js
          - test/integration/services/offer-bookshelf-repository.test.js
          - test/integration/services/offers-api.test.js
    unverified: []
    blockers:
      - >-
        Concurrent init is unpinned; root shape, pending, failed and repeated init and retained
        frontend/Members consumers are pinned by units. New composition must deliberately preserve
        or change publication timing and old listener/manager ownership, with cleanup evidence.
      - >-
        Subdirectory redirects are pinned only through the root with a real redirect manager over
        faked models. Initial rows, created/changed codes, archived offers and query strings are
        also pinned through the mounted site app and a real database.
      - >-
        Cover payment coupon failures after commit. Commit-then-dispatch and rollback without
        dispatch or an unhandled rejection are pinned with a real transaction.
        Define pending DB/provider work ownership before adding shutdown; no root drain exists.
      - >-
        Test concurrent coupon import and retention activation with the real database. Simulated
        duplicate errors and sequential retention assertions do not prove isolation, duplicate
        recovery visibility or one active retention offer per cadence under concurrent writes.
      - >-
        Extend the missing-product list test to other malformed rows, direct reads/startup,
        count-query/reporting errors and consumer fallbacks.
        Keep supplied-transaction reuse separate from operations that always open a transaction,
        and preserve the distinction between durable offer creation and later coupon provisioning.
  permissions:
    kind: directory
    disposition: migrate
    futureHome: core/server/auth/permissions
    status: audited
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: straightforward
      modelImpact: fits
      reason: >-
        Boot already awaits one action-map load and runtime callers then use
        canThis/parseContext. A facade can own the loaded map while each authorization check
        keeps its existing request-specific permission snapshot. Preserve the User model's
        deferred import, empty-map behavior and any explicit test reload path instead of
        treating repeated init as ordinary operation.
      reviewedAt: f276513ba32b7114fd5cc4f7b02d3f82c0c31e3e
      sources:
        - core/server/services/permissions/index.js
        - core/server/services/permissions/actions-map-cache.js
        - core/server/services/permissions/can-this.js
        - core/server/models/user.js
        - core/boot.js
      dependencies:
        complexity: complex
        reason: >-
          canThis and its providers load the model registry directly while User.permissible calls
          back into permissions through a deliberately deferred import. The action map is a
          module global. Dependency injection needs an explicit shared
          authorization graph without turning the existing model/permissions cycle into eager
          construction.
        preparation:
          - >-
            Create providers and canThis from injected models and model-rule helpers, sharing one
            owned action map.
          - >-
            Keep the User model callback deferred until authorization runs; cover both model-first
            and permission-first loading before switching consumers to a guarded service getter.
        reviewedAt: 44ad405bff4a8700af2ed51f833ebc08d25dddff
        sources:
          - core/server/services/permissions/index.js
          - core/server/services/permissions/can-this.js
          - core/server/services/permissions/providers.js
          - core/server/services/permissions/actions-map-cache.js
          - core/server/models/user.js
          - core/server/api/endpoints/utils/permissions.js
    entryPoints:
      - permissions/index.js
    notes: >-
      Suggested future home after this migration: server/auth/permissions; action maps, actor context and staff/API-key
      roles belong with the existing authorization owner. This remains configured application
      policy, not Kernel machinery. Boot still owns readiness; no source has moved. Preserve
      boot-loaded action definitions, per-check permission loading and model overrides. Returned
      maps and retained checks have different lifetimes; reload, early calls and failures still
      need focused contracts before changing initialization.
    contracts:
      exports:
        summary: >-
          CommonJS exports receiver-independent init, canThis and parseContext. Init returns a
          promise for the actual mutable action map. canThis synchronously builds a fresh chained
          result with nonenumerable, nonwritable action properties and object-type functions;
          invoking a handler returns a promise and may return a model override's result, including
          excludedAttrs. parseContext returns a new normalized object, retaining supplied user,
          key and member references, and integration only when api_key is supplied. Units pin chain methods and selected result/error
          shapes, not descriptors, detached exports or returned-map mutation.
        sources:
          - core/server/services/permissions/index.js
          - core/server/services/permissions/can-this.js
          - core/server/services/permissions/parse-context.js
          - core/server/services/permissions/actions-map-cache.js
          - core/server/models/post.js
        tests:
          - test/unit/server/services/permissions/index.test.js
          - test/unit/server/services/permissions/can-this.test.js
          - test/unit/server/services/permissions/parse-context.test.ts
      construction:
        summary: >-
          Import loads shared models, providers and one module-level empty actions cache without
          querying permissions or constructing a service class. Every init rebuilds the map from
          the permissions defined in code (role-permissions all()); it does no I/O.
          Action/object pairs are deduplicated. canThis starts
          user/key loads and builds handlers against a cloned action map and model references at
          check creation. Units assert the map built from the static data; import purity and
          provider-query timing are source-only.
        sources:
          - core/server/services/permissions/index.js
          - core/server/services/permissions/actions-map-cache.js
          - core/server/services/permissions/can-this.js
          - core/server/services/permissions/providers.js
          - core/server/services/permissions/role-permissions.ts
        tests:
          - test/unit/server/services/permissions/index.test.js
          - test/unit/server/services/permissions/can-this.test.js
      consumers:
        summary: >-
          Boot initializes the root. The API permission stage normalizes context, bypasses public
          requests, invokes dynamically selected checks and removes returned excludedAttrs.
          Schedules, ownership transfer, member-field definitions, gift links and audience feedback
          call checks directly. Role serialization retains canThis and suppresses failed assignment
          checks; User.permissible requires the root lazily to avoid a model import cycle and checks
          role changes. Real HTTP tests pin selected token/session role outcomes, persisted user-role
          changes, assignable-role snapshots and gift browse denials. They do not establish every
          model/endpoint override, early caller, serializer failure or retained check after reload.
        sources:
          - core/boot.js
          - core/server/api/endpoints/utils/permissions.js
          - core/server/api/endpoints/schedules.js
          - core/server/api/endpoints/users.js
          - core/server/api/endpoints/member-metafields.ts
          - core/server/api/endpoints/gift-links.ts
          - core/server/services/audience-feedback/audience-feedback-controller.js
          - core/server/api/endpoints/utils/serializers/output/roles.js
          - core/server/models/user.js
        tests:
          - test/e2e-api/admin/api-tokens.test.js
          - test/e2e-api/admin/users.test.js
          - test/e2e-api/admin/roles.test.js
          - test/e2e-api/admin/gift-links.test.ts
      startup:
        summary: >-
          Boot awaits init in its main service Promise.all after database setup, before readiness;
          imports and parseContext need no populated map. canThis throws synchronously when the
          map is empty, including internal requests. Init builds the map from static data, so a
          successful init populates it; it is also empty before init, after a rebuild that throws
          (see failures), or if a caller empties the mutable map init returns (see lifetime). No
          explicit ready flag is set. The pre-init unit
          stubs empty() rather than loading a fresh root. Booted HTTP suites establish successful
          composition, not empty data, init rejection, frontend-disabled ordering or the absence
          of authorization calls from other concurrently initializing services.
        sources:
          - core/boot.js
          - core/server/services/permissions/index.js
          - core/server/services/permissions/can-this.js
          - core/server/services/permissions/actions-map-cache.js
        tests:
          - test/unit/server/services/permissions/index.test.js
          - test/e2e-api/admin/api-tokens.test.js
      lifetime:
        summary: >-
          There is no init guard, in-flight sharing, invalidation subscription or stop/reset hook.
          Each init replaces the cache synchronously.
          getAll clones it, but init returns its mutable object directly. Existing checks retain
          their handler set and one user/key permissions promise; calling a handler again does not
          reload authorization. New checks load fresh user/key data, while action definitions stay
          unchanged until init. These retention, mutation, concurrent reload and repeated-boot
          contracts are source-only; current units recreate checks against fixture maps.
        sources:
          - core/server/services/permissions/index.js
          - core/server/services/permissions/actions-map-cache.js
          - core/server/services/permissions/can-this.js
          - core/server/services/permissions/providers.js
        tests: []
      configuration:
        summary: >-
          No config/settings/URL values are read by the root; permissions-data.ts defines available
          chains. User providers require active status and take the deduplicated union of their
          roles' permissions from code; API-key providers use their single role, and content keys
          (no role) get none. Authorization matches
          action/object, with model-specific rules responsible for resource restrictions. Owner
          user roles bypass the generic user-permission check. When both user and key exist, user
          permissions win and model overrides receive apiKey:null, although both providers load.
          parseContext assigns public from key type after user/internal handling and copies member
          without making it nonpublic. Units pin ordinary normalization, inactive users, dedup and
          staff-key precedence; mixed internal/content-key contexts and every model rule are untested.
        sources:
          - core/server/services/permissions/parse-context.js
          - core/server/services/permissions/providers.js
          - core/server/services/permissions/can-this.js
          - core/server/models/role-utils.ts
          - core/server/models/role.js
          - core/server/models/relations/authors.js
          - core/server/models/post.js
        tests:
          - test/unit/server/services/permissions/parse-context.test.ts
          - test/unit/server/services/permissions/providers.test.ts
          - test/unit/server/services/permissions/can-this.test.js
          - test/e2e-api/admin/api-tokens.test.js
      resources:
        summary: >-
          Owns the action map and per-check handlers/promises; models borrow the shared database
          pool. No client, timer, worker, listener, transaction, drain or cleanup registration is
          created here. User and key reads begin independently and have no shared transaction or
          cancellation; a model override can perform further reads. Internal handlers immediately
          resolve without awaiting those already-started provider promises when an internal context
          also contains a user/key. Pending-query shutdown, unused checks and rejection handling in
          that mixed internal path have no focused coverage.
        sources:
          - core/server/services/permissions/can-this.js
          - core/server/services/permissions/providers.js
          - core/server/data/db/connection.ts
          - core/server/ghost-server.ts
          - core/server/models/relations/authors.js
          - core/server/models/user.js
        tests: []
      failures:
        summary: >-
          Init reads no data, so it cannot fail on I/O; a throwing rebuild can leave an
          empty/partial replacement. Missing users/keys and inactive
          users reject, as do denied checks; model override errors/results propagate. API support
          rewrites NoPermission messages, preserves Ghost errors and wraps other failures; role
          serialization instead omits roles on any check failure. Provider and canThis units pin
          selected missing/inactive/denied and override errors with fake queries, not DB failures,
          partial rebuild, return-map corruption or recovery after a failed boot/reload.
        sources:
          - core/server/services/permissions/index.js
          - core/server/services/permissions/actions-map-cache.js
          - core/server/services/permissions/providers.js
          - core/server/services/permissions/can-this.js
          - core/server/api/endpoints/utils/permissions.js
          - core/server/api/endpoints/utils/serializers/output/roles.js
        tests:
          - test/unit/server/services/permissions/providers.test.ts
          - test/unit/server/services/permissions/can-this.test.js
    unverified: []
    blockers:
      - >-
        Pin real uninitialized-root behavior, boot
        failure/recovery and authorization ordering against other boot tasks. The current pre-init
        test stubs cache.empty and does not establish a fresh module's state or readiness gate.
      - >-
        Characterize repeated/overlapping init, retained chains, per-check permission snapshots,
        returned-map mutation and partial rebuild errors. Preserve module import-cycle behavior;
        User.permissible deliberately loads permissions only when needed.
      - >-
        Extend real consumer contracts for excludedAttrs, model overrides, direct endpoint calls
        and role serializer failure policy. Existing token/role HTTP tests cover selected routes,
        not all authorization paths or changing roles/keys during a retained check.
      - >-
        Cover failed and unused provider promises, internal contexts also containing user/key,
        malformed/mixed context and pending database shutdown. Internal handlers bypass awaiting
        provider loads already started by canThis; no cancellation or root drain is present.
  post-scheduling:
    kind: directory
    disposition: migrate
    status: audited
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: complex
      modelImpact: fits
      reason: >-
        The constructor installs six listeners and registers itself with a shared adapter;
        boot separately runs that adapter and later rebuilds jobs, while key rotation also
        invokes the retained rescheduler. Move construction without merging those owners,
        and define rollback of subscriptions/registration if startup fails. One retained
        scheduler facade fits; job completion and normal adapter drain remain separate.
      reviewedAt: f276513ba32b7114fd5cc4f7b02d3f82c0c31e3e
      sources:
        - core/server/services/post-scheduling/index.ts
        - core/server/services/post-scheduling/post-scheduling.ts
        - core/server/adapters/scheduling/error-capture.ts
        - core/server/auth/reset-authentication.ts
        - core/boot.js
      dependencies:
        complexity: straightforward
        reason: >-
          The root already supplies the scheduling adapter, API URL and internal keys. Models, event
          bus, logging and signed-job helpers remain imported inside the class, but can be injected
          into that same constructor. Adapter registration and rescheduling timing need lifecycle
          care; they do not add a dependency cycle or require a new container.
        preparation:
          - >-
            Inject the Post model, event bus, logger and job-building/idempotency functions from
            index.ts, and import Moment through the shared clock.
          - >-
            Construct after adapter/internal-key availability while preserving the existing separate
            adapter.run and rescheduleAll boot steps.
        reviewedAt: 44ad405bff4a8700af2ed51f833ebc08d25dddff
        sources:
          - core/server/services/post-scheduling/index.ts
          - core/server/services/post-scheduling/post-scheduling.ts
          - core/server/adapters/scheduling/build-signed-job.ts
          - core/server/adapters/scheduling/get-scheduler-idempotency-key.ts
          - core/boot.js
    entryPoints:
      - post-scheduling/index.ts
    notes: >-
      Preserve constructor event subscriptions, shared adapter identity, signed callback
      generation and boot/key-rotation rebuilding. The publishing callback belongs to the
      separate posts composition; the scheduler graph still lacks cleanup ownership.
    contracts:
      exports:
        summary: >-
          TypeScript default-exports one eagerly constructed PostScheduling instance;
          CommonJS boot reads .default. Its only public operation is receiver-dependent
          async rescheduleAll({previousKey} = {}), resolving undefined after database/key
          lookup and adapter call submission, not after those calls settle. The deep module
          default-exports the constructor; production constructs it only in the root. There
          is no init/reset/stop export. Class tests invoke rescheduleAll directly but do not
          assert root export shape, singleton identity or pending-adapter completion semantics.
        sources:
          - core/server/services/post-scheduling/index.ts
          - core/server/services/post-scheduling/post-scheduling.ts
          - core/boot.js
        tests:
          - test/unit/server/services/post-scheduling/post-scheduling.test.js
      construction:
        summary: >-
          Import captures the Admin API URL, resolves the adapter-manager's cached scheduling
          adapter and creates an ErrorCapturingSchedulingAdapter around it. The constructor
          retains URL/adapter/internal-keys, adds six common-event listeners for scheduled,
          rescheduled and unscheduled posts/pages, then registers itself on the adapter.
          Boot makes a separate error-capture wrapper around the same cached adapter; auth
          retains the underlying adapter for rotation. Constructor does not start it. The
          class unit titled starts the adapter only checks a post.scheduled job's arguments
          with run/execution/network stubbed; it does not assert registration or root identity.
        sources:
          - core/server/services/post-scheduling/index.ts
          - core/server/services/post-scheduling/post-scheduling.ts
          - core/kernel/adapter-manager/adapter-manager.ts
          - core/server/adapters/scheduling/error-capture.ts
          - core/server/api/endpoints/authentication.js
          - core/boot.js
        tests:
          - test/unit/server/services/post-scheduling/post-scheduling.test.js
      consumers:
        summary: >-
          Boot directly rebuilds the root. Post model changes emit the six subscribed event
          names; transactional events are deferred until committed, but their async handlers
          are not awaited. Adapter registration enables auth reset's rebuild with the previous
          key. Jobs call the signed Admin schedules endpoint, which delegates publishing to
          posts/post-scheduling, not this instance. That module reads/edits the appropriate API,
          applies time/status checks and has a process-local in-flight guard. Legacy HTTP
          tests manually call callbacks with automatic pings disabled: posts/pages publish,
          early/missing resources no-op, late unforced requests fail and overlapping requests
          yield one published response and one email row. A deferred-read unit additionally
          pins the overlap no-op while the first publish is pending; neither proves adapter delivery.
        sources:
          - core/boot.js
          - core/server/models/post.js
          - core/server/models/base/plugins/events.js
          - core/server/services/post-scheduling/post-scheduling.ts
          - core/server/auth/reset-authentication.ts
          - core/server/api/endpoints/authentication.js
          - core/server/web/api/endpoints/admin/routes.js
          - core/server/api/endpoints/schedules.js
          - core/server/services/posts/post-scheduling.js
        tests:
          - test/legacy/api/admin/schedules.test.js
          - test/unit/server/services/post-scheduling/post-scheduling.test.js
          - test/unit/server/auth/reset-authentication.test.ts
      startup:
        summary: >-
          Boot validates adapters before importing this eager root in initServices. It later
          calls the shared adapter's run without awaiting a possible promise, initializes
          dependent services, registers job handlers and awaits jobs.start. After synchronous
          automations init returns, it awaits post rescheduleAll only when rescheduleOnBoot
          is truthy. Rebuilding queries scheduled posts and pages concurrently and then reads
          the scheduler key. Boot harness tests keep jobs.start unresolved and assert neither
          automations nor rescheduling runs, propagate its rejection, and assert opt-out skips
          rescheduling. Modules are fakes, so real adapter readiness/root import ordering and
          outstanding adapter operations are not protected by those assertions.
        sources:
          - core/boot.js
          - core/server/services/post-scheduling/index.ts
          - core/server/services/post-scheduling/post-scheduling.ts
          - core/server/adapters/scheduling/error-capture.ts
        tests:
          - test/unit/boot/init-services.test.ts
      lifetime:
        summary: >-
          Module caching preserves the first URL/adapter graph and six listeners. There is
          no listener disposer, rebind or registration removal; constructing another instance
          adds another listener set. Reschedulers remain retained by the shared adapter.
          Database scheduled status is durable; the default adapter's queue, tombstones and
          running flag are process state. Normal rebuild unschedules then schedules using
          bootstrap=true, except adapters declaring strict-true idempotency dedupe skip the
          unschedule. Rotation always unschedules with the previous key and bootstrap=false.
          Class units assert those calls/flags, stable selected job keys and changed keys for
          time/key changes; they do not prove remote completion order, durable dedupe or restart.
        sources:
          - core/server/services/post-scheduling/post-scheduling.ts
          - core/server/adapters/scheduling/scheduling-default.ts
          - core/server/adapters/scheduling/error-capture.ts
          - core/kernel/adapter-manager/adapter-manager.ts
        tests:
          - test/unit/server/services/post-scheduling/post-scheduling.test.js
      configuration:
        summary: >-
          Root captures the URL and resolved adapter at import; later adapter-manager cache
          clearing does not rebind it. Shared URL bases/config are frozen, so repeated facade
          calls and test replacement are not hot reload. Internal keys are read per event or
          rebuild through a retained promise cache, cleared by auth rotation. Jobs use current
          published_at, previous published_at for unscheduling, PUT and oldTime metadata;
          callback URL/time determine the namespaced hash. JWTs start ten minutes before
          publish and normally expire six hours after; sufficiently overdue jobs instead
          expire six hours from signing, so later rebuilds can change their URL/hash even
          without changing the key or publish time. Tests compare selected arguments/hash
          behavior, not JWT boundaries or that overdue case.
        sources:
          - core/server/services/post-scheduling/index.ts
          - core/server/services/post-scheduling/post-scheduling.ts
          - core/server/adapters/scheduling/build-signed-job.ts
          - core/server/adapters/scheduling/utils.ts
          - core/server/adapters/scheduling/get-scheduler-idempotency-key.ts
          - core/server/services/internal-keys/index.ts
          - core/server/lib/auto-filling-map.ts
          - core/server/auth/reset-authentication.ts
          - core/shared/url-utils.ts
          - core/shared/config/loader.ts
        tests:
          - test/unit/server/services/post-scheduling/post-scheduling.test.js
          - test/unit/server/adapters/scheduling/get-scheduler-idempotency-key.test.ts
      resources:
        summary: >-
          This root owns subscriptions and its error-capture wrapper, borrowing the shared
          scheduler, models and key cache. The default adapter owns in-memory jobs/tombstones,
          recurring five-minute wakeups, per-job timers/immediates, retry timers and pending
          HTTP calls through shared request/DNS support. It has no stop/drain; neither this
          root nor boot registers scheduler cleanup with GhostServer. Default-adapter units
          use fake timers to assert selected queue/tombstone/ping behavior and a request fake
          for method/force flags and two 503 retries. They do not test root shutdown, network
          completion, persistent adapters or cleanup across same-process boot cycles.
        sources:
          - core/server/services/post-scheduling/post-scheduling.ts
          - core/server/adapters/scheduling/error-capture.ts
          - core/server/adapters/scheduling/scheduling-default.ts
          - core/kernel/adapter-manager/adapter-manager.ts
          - core/server/lib/common/events.js
          - core/boot.js
          - core/server/ghost-server.ts
        tests:
          - test/unit/server/adapters/scheduling/scheduling-default.test.js
      failures:
        summary: >-
          Adapter resolution/registration can fail import, and registration occurs after
          listeners were attached. Event handlers log key/normalization or synchronous raw
          adapter failures; they do not retry. Production error-capture reports synchronous
          throws and rejected scheduling promises without propagating them. Direct rebuild
          query/key/normalization failures reject, possibly after earlier jobs were submitted;
          adapter-wide rotation observes async rescheduler failures as settled results.
          Unschedule/schedule are not awaited, so persistent-adapter completion can reorder.
          Default HTTP requests have five-second timeout/no client retries; only status 503
          schedules up to 30 delayed retries, 404 is ignored and other errors are logged.
          Decorator units assert selected reporting/redaction and rejection handling; they
          do not establish whole-root recovery, and the two-retry unit does not pin exhaustion.
        sources:
          - core/server/services/post-scheduling/index.ts
          - core/server/services/post-scheduling/post-scheduling.ts
          - core/server/adapters/scheduling/error-capture.ts
          - core/server/adapters/scheduling/scheduling-default.ts
          - core/server/adapters/scheduling/README.md
          - core/server/auth/reset-authentication.ts
        tests:
          - test/unit/server/adapters/scheduling/error-capture.test.ts
          - test/unit/server/adapters/scheduling/scheduling-default.test.js
    unverified: []
    blockers:
      - >-
        Protect root/default export identity, six listener registrations and adapter registration
        through repeated construction, failed import and stop/reboot. Define ownership of
        listener removal, scheduler timers, pending HTTP work and adapter rebinding; none is
        supplied by the existing root or default adapter.
      - >-
        Cover real post/page schedule, reschedule and unschedule events through commit/rollback
        and overlapping edits, including previous timestamp preservation and key failures.
        The constructor unit drives only post.scheduled; manually called HTTP callbacks do not
        establish queue registration or event completion ordering.
      - >-
        Characterize real adapter readiness and outstanding operations during boot/rotation,
        rejection, shutdown and partial rebuild. Invocation-order assertions do not protect
        remote unschedule/schedule completion order or persistence; root calls intentionally
        return before adapter promises settle.
      - >-
        Protect scheduled callback signing/expiry and recovery for jobs overdue by more than
        six hours. Their generated expiry and URL/hash can change with the clock, which is
        outside the existing same-job key assertions and can affect dedupe or cancellation.
      - >-
        Preserve callback no-op/force/authentication/cache behavior with the separate posts
        owner, and cover publish failure/retry, process restart and cross-process duplicate
        delivery. Current overlap guard is process-local; a single email row in the HTTP test
        does not prove provider delivery exactly once or default-adapter retry of every failure.
  posts:
    kind: directory
    disposition: migrate
    status: audited
    auditedAt: c2999f4e13da6360bceced808bb52c4a04b5ad03
    migration:
      complexity: complex
      modelImpact: fits
      reason: >-
        This remains a business service. Several endpoints, the output mapper, Explore and
        Lexical currently retain separately constructed graphs that capture email-service
        immediately. Consolidate composition only after email readiness and verify retained
        consumers. Export state is already per operation and the scheduling publisher owns a
        separate process-wide guard, so neither needs to become shared mutable instance
        state.
      reviewedAt: f276513ba32b7114fd5cc4f7b02d3f82c0c31e3e
      sources:
        - core/server/services/posts/posts-service-instance.js
        - core/server/services/posts/posts-service.js
        - core/server/services/posts/posts-exporter.js
        - core/server/services/posts/post-scheduling.js
        - core/server/api/endpoints/utils/serializers/output/mappers/posts.js
      dependencies:
        complexity: complex
        reason: >-
          The factory builds fresh Posts, export and statistics graphs for API callers, while
          Lexical caches a separately created Posts instance and Email rendering uses Lexical. The
          directory also contains a separate scheduled-publish path importing the API layer.
          Retaining one root therefore changes dependency identity and exposes deferred cycles, not
          just constructor syntax.
        preparation:
          - >-
            Map and replace the fresh API and cached Lexical compositions with an agreed retained
            Posts facade; preserve deferred Email/Lexical access instead of forcing eager capture.
          - >-
            Extract explicit collaborators for post statistics, email actions, export formatting,
            transaction completion and event dispatch, with root-owned models/logging/config.
          - >-
            Give the scheduled-publish API adapter a clear owner and injected API operations so it
            does not remain an independent hidden composition inside Posts.
        reviewedAt: 44ad405bff4a8700af2ed51f833ebc08d25dddff
        sources:
          - core/server/services/posts/posts-service-instance.js
          - core/server/services/posts/posts-service.js
          - core/server/services/posts/post-email-handler.js
          - core/server/services/posts/stats/post-stats.js
          - core/server/services/posts/post-scheduling.js
          - core/server/lib/lexical.js
          - core/server/api/endpoints/posts.js
    entryPoints:
      - posts/posts-service-instance.js
      - posts/post-scheduling.js
    notes: >-
      Record factory-created caller lifetimes and the separate scheduling module state;
      establish which identity boundaries must survive before choosing shared composition.
      The pilot needs focused construction/readiness and retained-consumer checks, not a
      rewrite of unrelated post business logic.
    contracts:
      exports:
        summary: >-
          There is no directory index. CommonJS posts-service-instance exports a synchronous
          zero-argument factory with PostsService attached for tests. Each result exposes
          browse/read/edit, bulk operations, export, visibility mapping, copying, cache helpers
          and public collaborators including stats and postEmailHandler. Most methods require
          their receiver; generateCopiedPostLocationFromUrl is passed unbound by posts/pages.
          The separate post-scheduling module exports publish and handleCacheInvalidation.
          Units assert selected copy/cache/edit results and handler dependencies; root/factory
          export identity and repeated construction are source-only.
        sources:
          - core/server/services/posts/posts-service-instance.js
          - core/server/services/posts/posts-service.js
          - core/server/services/posts/post-email-handler.js
          - core/server/services/posts/post-scheduling.js
          - core/server/api/endpoints/posts.js
          - core/server/api/endpoints/pages.js
        tests:
          - test/unit/server/services/posts/posts-service.test.js
          - test/unit/server/services/posts/post-email-handler.test.js
      construction:
        summary: >-
          Every factory call creates PostStats, PostsExporter and PostsService, which creates
          PostEmailHandler. It captures email-service.service immediately, shares the models
          object with the handler, and gives the exporter a separate object of captured model
          constructors. PostStats retains the shared DB facade and resolves knex on use. Other
          dependencies are retained URL/settings helpers plus an isSet closure; the latter is
          stored but unused by current PostsService methods. Constructors do no queries or
          subscriptions themselves, although imports acquire shared modules. No guard, init or
          singleton cache exists in the factory. Handler constructor tests assert injected
          model/email identity; no test directly exercises the real factory dependency graph.
        sources:
          - core/server/services/posts/posts-service-instance.js
          - core/server/services/posts/posts-service.js
          - core/server/services/posts/post-email-handler.js
          - core/server/services/posts/posts-exporter.js
          - core/server/services/posts/stats/post-stats.js
        tests:
          - test/unit/server/services/posts/post-email-handler.test.js
      consumers:
        summary: >-
          Posts/pages, public-post browse, admin/public search indexes, site exports and the
          post output mapper each retain a factory result at module evaluation. Their uses
          include edit/email, bulk/copy/cache headers, browse, export and legacy visibility
          conversion; several other endpoint actions bypass this service and use models
          directly. ExplorePingService receives a new result per construction and uses stats.
          Lexical render/validate lazily creates and retains a result, but currently never
          invokes it. The schedules endpoint calls the separate publisher. Search controller
          units replace PostsService.prototype.browsePosts and assert URL-required columns;
          real HTTP tests assert search projections, CSV/copy responses and bulk DB results.
          Newsletter integration and retry HTTP tests exercise the real post/email connection
          with provider or scheduling seams. Atomic-publication integration tests also exercise
          transaction rollback and dispatch after commit. They do not pin factory/caller
          identity timing.
        sources:
          - core/server/api/endpoints/posts.js
          - core/server/api/endpoints/pages.js
          - core/server/api/endpoints/posts-public.js
          - core/server/api/endpoints/search-index.js
          - core/server/api/endpoints/search-index-public.js
          - core/server/api/endpoints/exports.js
          - core/server/api/endpoints/utils/serializers/output/mappers/posts.js
          - core/server/services/explore-ping/index.ts
          - core/server/services/explore-ping/explore-ping-service.ts
          - core/server/lib/lexical.js
          - core/server/api/endpoints/schedules.js
        tests:
          - test/unit/api/endpoints/search-index.test.js
          - test/unit/api/endpoints/search-index-public.test.js
          - test/e2e-api/admin/search-index.test.js
          - test/e2e-api/content/search-index.test.js
          - test/e2e-api/admin/posts.test.js
          - test/e2e-api/admin/posts-bulk.test.js
          - test/integration/services/email-service/batch-sending.test.js
          - test/integration/services/email-service/atomic-publish.test.ts
          - test/e2e-api/admin/email-retry.test.ts
      startup:
        summary: >-
          Boot has no posts initialization call. Factory readiness depends on the operation:
          model/DB reads can work without email, while publication preflight/create/retry needs
          the email implementation captured at construction. API pipeline getters and lazy
          routers defer many caller imports; boot initializes email before mounting/readiness,
          and Explore constructs posts during its later invocation in that service group.
          Direct/early imports and Lexical construction are not guarded against capturing an
          absent email service. Current HTTP tests prove selected normal boot/request paths,
          not initialization order for retained callers or rejection before readiness. A new
          managed root must explicitly coordinate these existing construction sites with boot.
        sources:
          - core/boot.js
          - core/server/services/posts/posts-service-instance.js
          - core/server/services/posts/post-email-handler.js
          - core/server/api/endpoints/index.js
          - core/shared/express.js
          - core/server/web/api/app.js
          - core/server/web/api/endpoints/admin/routes.js
          - core/server/lib/lexical.js
          - core/server/services/explore-ping/index.ts
        tests:
          - test/integration/services/email-service/batch-sending.test.js
          - test/e2e-api/admin/email-retry.test.ts
      lifetime:
        summary: >-
          Factory instances persist as long as callers retain them, with mutable public
          collaborator fields and an independently retained email reference inside the handler.
          Replacing email-service.service does not update either reference; replacing the
          outer PostsService.emailService alone does not update its handler. Production callers
          inspected do not replace these fields; units do replace postEmailHandler. No reset or
          shutdown is exposed. Export state is per invocation. Scheduling has a module-wide
          inFlight Set keyed by resource type/id and a shared no-op object; publish releases
          the key in finally. Its gated unit proves same-key overlap returns no-op while the
          first read is held, then one edit occurs. Error release, independent keys and
          retained-consumer behavior across initialization remain source-only.
        sources:
          - core/server/services/posts/posts-service.js
          - core/server/services/posts/post-email-handler.js
          - core/server/services/posts/posts-service-instance.js
          - core/server/services/posts/posts-exporter.js
          - core/server/services/posts/post-scheduling.js
        tests:
          - test/unit/server/services/posts/posts-service.test.js
          - test/unit/server/services/post-scheduling/post-scheduling.test.js
      configuration:
        summary: >-
          Factory captures helper/model/email objects rather than a config subtree. Export
          awaits newsletter/label/tier reads, then snapshots member, paid-member, tracking and
          feedback settings for that stream; later exports read the retained facades again.
          URL generation calls the shared output helper for each row. Scheduling reads the
          configured minute tolerance at the beginning of each publish attempt, then uses that
          captured value after its API read. PostStats has fixed published/sent filters.
          Export units assert selected column changes after fake settings changes, default
          limit 15, requested limits and order forwarding. This establishes per-call behavior
          with fakes, not config hot reload or dependency rebinding; immutable config rules
          still apply. Real factory capture/config timing lacks a dedicated test.
        sources:
          - core/server/services/posts/posts-service-instance.js
          - core/server/services/posts/posts-exporter.js
          - core/server/services/posts/post-scheduling.js
          - core/server/services/posts/stats/post-stats.js
        tests:
          - test/unit/server/services/posts/posts-exporter.test.js
      resources:
        summary: >-
          The factory graph owns no recurring timer, subscriber or provider client. It borrows
          DB/models, email and URL/settings helpers. Edit creates or borrows a transaction for
          the post and new email, then schedules or retries after commit. A caller-owned
          transaction retains that callback through its executionPromise; this service has no
          pending-callback registry or drain. Bulk operations use local transactions and dispatch
          domain events without waiting for subscribers. Export owns a Readable over an async generator, one metadata
          context and bounded pages of up to 50 rows; the HTTP serializer owns its CSV transform
          and pipeline to the response, while site export owns its enclosing pipeline. Export
          units consume the stream and assert 75 exact IDs across two 50-row queries. This does
          not establish early-close cancellation of an already-running DB query. Scheduling
          owns only the active-key guard, with no timeout/drain; queued timer ownership belongs
          to the separate post-scheduling service/adapter, not this module.
        sources:
          - core/server/services/posts/posts-service.js
          - core/server/services/posts/posts-exporter.js
          - core/server/services/posts/post-scheduling.js
          - core/server/services/posts/stats/post-stats.js
          - core/server/lib/after-commit.ts
          - core/server/api/endpoints/utils/serializers/output/posts.js
          - core/server/api/endpoints/utils/serializers/output/stream-csv-response.ts
          - core/server/api/endpoints/utils/serializers/output/stream-response.ts
          - core/server/api/endpoints/exports.js
        tests:
          - test/unit/server/services/posts/posts-exporter.test.js
          - test/e2e-api/admin/posts.test.js
      failures:
        summary: >-
          editPost awaits preflight, then saves the post and any new newsletter email in one
          transaction. With its own transaction it awaits scheduling/retry after commit,
          serializes and awaits the optional event callback. A caller-owned transaction instead
          registers scheduling/retry after its executionPromise resolves; edit does not wait
          for that commit, and later send errors are logged. Preflight failure prevents edit;
          email-insert failure rolls back publication, but a post-commit failure can reject
          after publication. Handler wraps invalid recipient-filter queries as BadRequest,
          warns without retrying an unknown delivery outcome, and tolerates a lost retry claim
          only when the refreshed email is pending/submitting/submitted. Units assert selected
          arguments, send-after-commit ordering, caller-commit gating and logged send failure.
          Integration tests assert rollback on email-insert failure and an outer transaction
          error, dispatch after caller commit, scheduled-publication retry, one email for two
          overlapping edits, and rejection after a committed post when retry scheduling fails.
          These do not prove delivery completion or every held completion/error boundary;
          rollback without an error is also not covered and the helper treats a resolved
          executionPromise as a commit.
          Bulk methods propagate write failures and dispatch after their awaited local work;
          no whole-operation/subscriber atomicity is implied. Export-context failure rejects
          before returning a stream, later page/mapping failure reaches the stream; invalid
          human-readable filters log and fall back. Scheduling treats missing/non-scheduled/
          early resources as no-op, keeps overdue non-forced publishes as errors and propagates
          other API failures. Legacy real HTTP cases assert publish/no-op/status and concurrent
          one-email outcomes with outbound scheduler/provider mocks; they do not prove every
          transaction rollback, guard-release failure or delivery completion.
        sources:
          - core/server/services/posts/posts-service.js
          - core/server/services/posts/post-email-handler.js
          - core/server/services/posts/posts-exporter.js
          - core/server/services/posts/post-scheduling.js
          - core/server/lib/after-commit.ts
          - core/server/api/endpoints/schedules.js
        tests:
          - test/unit/server/services/posts/posts-service.test.js
          - test/unit/server/services/posts/post-email-handler.test.js
          - test/unit/server/services/posts/posts-exporter.test.js
          - test/integration/services/email-service/atomic-publish.test.ts
          - test/e2e-api/admin/email-retry.test.ts
          - test/legacy/api/admin/schedules.test.js
    unverified: []
    blockers:
      - >-
        Pilot gate: characterize the real factory's fresh identities/shared collaborators and
        immediate email capture, then prove the proposed managed facade remains usable by
        retained endpoint/mapper/Explore/Lexical consumers. Import-time availability and
        call-time readiness must be explicit; current business tests do not pin those boundaries.
      - >-
        Pilot gate: add the smallest boot/consumer assertion that publication reaches the
        initialized email implementation through the retained posts service. Preserve the
        real PostEmailHandler connection and existing DTO, preflight and retry results; a
        prototype-only controller stub would miss a broken facade/dependency reference.
      - >-
        Pilot gate if edit composition changes: hold preflight/email/event-callback completion and
        assert downstream calls/results remain pending, with original failures propagated at
        each boundary. Preserve the distinction between awaited sending after a local commit
        and logged sending failures after a caller commit. Existing units hold the caller's
        commit but replace the handler and do not hold every stage unresolved; reuse the
        atomic-publication integration and existing HTTP/provider-mock coverage.
      - >-
        If migration touches the separate scheduling module, preserve its process-wide guard
        and add failure-release/independent-key checks. Do not accidentally give each factory
        its own guard. Existing overlap unit and HTTP test cover the successful same-key case.
      - >-
        Wider characterization, not a prerequisite for a composition-only pilot: export
        interruption/page failure and transaction/subscriber failure boundaries remain partly
        source-only, and PostStats real query behavior has no dedicated assertion found.
        Preserve their existing owners and behavior unless the migration changes them.
  posts-public:
    kind: directory
    disposition: migrate
    futureHome: core/server/api/endpoints/utils/public-response-cache
    status: audited
    migrated: true
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: straightforward
      modelImpact: fits
      reason: >-
        The boot-owned default root returns one facade with an optional cache. Disabled
        caching still yields a ready instance. The endpoint captures service.cache after
        initialization, preserving adapter identity and the single site.changed listener.
        Registration remains the final resource acquisition; no new lifecycle hook is needed.
      reviewedAt: c538745fe27d8782fc3a6d847bb626d2b6ff130e
      sources:
        - core/server/services/posts-public/index.ts
        - core/server/api/endpoints/posts-public.js
        - core/boot.js
      dependencies:
        complexity: straightforward
        reason: >-
          The migrated root already wires the cache adapter, its enablement config and the
          site-change event bus in index.ts. The Content API captures the ready cache facade;
          this composition is independent of the separate Posts domain service migration.
        preparation: []
        reviewedAt: 326a1a3e711690149928fcb69e6fbc5516213ba9
        sources:
          - core/server/services/posts-public/index.ts
          - core/server/api/endpoints/posts-public.js
          - core/boot.js
    entryPoints:
      - posts-public/index.ts
    notes: >-
      Suggested future home after this migration: API-owned public-response-cache support; this root only selects the
      posts cache and subscribes to site invalidation; query, access and cache-key policy already
      live in the endpoint. Keep the boot-owned configured instance and readiness guard at the
      destination. No source has moved. The prior auditedAt records the pre-migration baseline.
      The bounded migration was inspected on main c538745fe27d8782fc3a6d847bb626d2b6ff130e; its
      existing root/binding, member/gift-key and isolated cold-boot HTTP tests passed before
      production changes. The default initializer replaces the wrapper without extras. Cache
      configuration, identity and invalidation are unchanged; early controller imports now fail
      through the readiness guard. Restart, adapter failures and asynchronous invalidation remain
      separate obligations below. Feature-local boolean parsing preserves legacy truthiness
      without tightening accepted configuration.
    contracts:
      exports:
        summary: >-
          The TypeScript root default-exports a defineService initializer with init and
          a guarded service getter; there are no extras. The ready service is one facade
          containing cache, including undefined when disabled. It has no query, reset or
          shutdown method. Unit assertions cover enabled/disabled cache values and endpoint
          identity; generic readiness, shared initialization and retry belong to kernel tests.
        sources:
          - core/kernel/define-service.ts
          - core/server/services/posts-public/index.ts
          - core/server/api/endpoints/posts-public.js
        tests:
          - test/unit/server/services/public-response-caches.test.ts
      construction:
        summary: >-
          Import declares the root without resolving runtime collaborators. First init
          requires the shared adapter manager, config facade and common event bus. A truthy
          postsPublicCache flag obtains cache:postsPublic and registers its reset listener
          as the final resource acquisition before returning the facade. Boot awaits init
          before the guarded service is ready. The manager caches adapters by requested name
          and class. No other production construction or deep consumer was found. The
          endpoint's separate PostsService composition remains unchanged. Units inject the
          adapter and assert its exact name, identity and absence when disabled.
        sources:
          - core/server/services/posts-public/index.ts
          - core/server/services/adapter-manager/index.ts
          - core/kernel/adapter-manager/adapter-manager.ts
          - core/server/api/endpoints/posts-public.js
          - core/server/services/posts/posts-service-instance.js
        tests:
          - test/unit/server/services/public-response-caches.test.ts
      consumers:
        summary: >-
          Boot initializes the root. Posts browse/read capture service.cache at endpoint module
          evaluation; the shared API pipeline preserves that adapter identity and caches its
          controller wrapper. Content routes, frontend data/entry lookup, get/collection and
          prev_post/next_post helpers, and llms all reach these endpoint methods. Pages have
          no response cache. Browse delegates to a separately retained PostsService; read
          restricts identifiers, fetches a Post and applies gift access before serialization.
          Binding units check both methods with caching enabled and disabled. Isolated HTTP
          tests prove repeated MemoryCache responses avoid model calls, fields changes miss,
          and explicit site.changed refreshes; they do not prove every frontend consumer or
          ordinary Admin mutations emit invalidation. Other Content tests assert selected
          hidden-selector rejection/ignoring and gated responses without enabling this cache.
        sources:
          - core/boot.js
          - core/server/api/endpoints/index.js
          - core/server/api/endpoints/posts-public.js
          - core/server/api/endpoints/pages-public.js
          - core/server/web/api/endpoints/content/routes.js
          - core/frontend/services/proxy.js
          - core/frontend/services/routing/config.ts
          - core/frontend/services/data/fetch-data.js
          - core/frontend/services/data/entry-lookup.js
          - core/frontend/helpers/get.js
          - core/frontend/helpers/collection.js
          - core/frontend/helpers/prev_post.js
          - core/frontend/services/llms/service.js
          - core/server/web/parent/middleware/emit-events.js
        tests:
          - test/unit/server/services/public-response-caches.test.ts
          - test/e2e-server/public-response-caches.isolated.test.ts
          - test/e2e-api/content/posts.test.js
          - test/e2e-api/content/gift-links.test.ts
      startup:
        summary: >-
          Boot validates adapters and prepares models/config before awaiting init in the
          existing service group, before mounting the full app. Content router construction
          is lazy; endpoint getters normally load the controller after initialization. The
          isolated HTTP suite exercises a cold boot with both cache flags enabled. Reading
          service while importing the controller before readiness now throws rather than
          silently binding no cache. The root does not initialize on a request, await cache
          readiness or register cleanup. No service-specific test injects construction failure
          or a later boot failure after this listener has been installed.
        sources:
          - core/boot.js
          - core/server/services/posts-public/index.ts
          - core/kernel/adapter-manager/adapter-manager.ts
          - core/server/api/endpoints/index.js
          - core/server/api/endpoints/posts-public.js
          - core/server/web/api/app.js
          - core/server/web/api/endpoints/content/app.js
          - core/server/web/api/endpoints/content/routes.js
          - core/shared/express.js
        tests:
          - test/e2e-server/public-response-caches.isolated.test.ts
      lifetime:
        summary: >-
          Successful init retains the same facade, adapter, entries and listener, including
          a facade with no cache when disabled. There is no listener disposer or supported
          service reset. Clearing the manager cache does not update retained root, listener
          or endpoint references. MemoryCache stores/returns response references and replaces
          its backing object on reset. Units assert repeated enabled init preserves cache
          identity and entries with one listener, and invoke only the newly installed reset
          callback. Isolated HTTP tests additionally emit on the real bus. Same-module restart,
          forced re-init, reference mutation and overlapping reads/reset remain source-only
          risks beyond those assertions.
        sources:
          - core/server/services/posts-public/index.ts
          - core/server/api/endpoints/posts-public.js
          - core/kernel/adapter-manager/adapter-manager.ts
          - core/server/adapters/cache/MemoryCache.js
          - core/server/lib/common/events.js
          - core/server/ghost-server.ts
        tests:
          - test/unit/server/services/public-response-caches.test.ts
          - test/e2e-server/public-response-caches.isolated.test.ts
      configuration:
        summary: >-
          The root reads hostSettings:postsPublicCache:enabled only on first init through a
          feature-local coercing boolean schema. It preserves legacy truthiness; missing/falsy
          disables. The manager resolves cache feature options
          and defaults to MemoryCache, retaining a constructed adapter for the same name/class.
          Config is frozen; test overrides do not establish hot reload. Request key data reads
          current options, member UUID, a free-status boolean and sorted tier slugs, plus read identifiers and resolved
          gift post ID. Key generation precedes cache lookup, so gift resolution still runs on
          a hit. Member-isolation units use real key/pipeline/gating code with an injected cache,
          fake query and skipped validation/permissions; they assert UUID separation, changed
          status/tiers and rejection of legacy entitlement-only cache entries. Gift units assert
          omission for absent gifts, unresolved-versus-absent separation and equal keys for tokens
          resolving to the same post, with stubbed resolution rather than cached revocation. Exact option normalization is source-only beyond the HTTP
          fields-variant assertion.
        sources:
          - core/server/services/posts-public/index.ts
          - core/kernel/adapter-manager/options.ts
          - core/kernel/adapter-manager/adapter-manager.ts
          - core/shared/config/defaults.json
          - core/shared/config/SCHEMA.md
          - core/server/api/endpoints/posts-public.js
          - core/server/api/endpoints/utils/public-cache-keys.ts
          - core/server/api/endpoints/utils/gift-link-access.ts
        tests:
          - test/unit/server/services/public-response-caches.test.ts
          - test/unit/api/endpoints/utils/public-cache-keys.test.ts
          - test/unit/api/endpoints/utils/gift-link-access.test.ts
          - test/e2e-server/public-response-caches.isolated.test.ts
      resources:
        summary: >-
          The root owns an unremoved common-event listener and borrows a manager-cached adapter.
          Default MemoryCache has no TTL, bound, timer or I/O. Optional memory-ttl owns a TTLCache
          with unref'd expiry timers, cleared by reset; Redis can share a process store/client,
          attaches an error listener and owns lookup timers, coalesced reads and background
          refresh tracking. Its timed-out lookup continues. Neither root nor adapter manager
          provides a cache shutdown/drain; Redis reset rotates a namespace rather than closing
          its client. Unit adapter tests assert memory clear/read/keys and selected fake-store
          Redis timeout, refresh and reset results; these do not cover a real posts endpoint
          using those adapters, resource disposal or completion at server shutdown.
        sources:
          - core/server/services/posts-public/index.ts
          - core/kernel/adapter-manager/adapter-manager.ts
          - core/server/adapters/cache/MemoryCache.js
          - core/server/adapters/cache/AdapterCacheMemoryTTL.js
          - core/server/adapters/cache/memory-ttl.js
          - core/server/adapters/cache/Redis.js
          - core/server/adapters/lib/redis/AdapterCacheRedis.js
          - core/server/adapters/lib/redis/redis-store-factory.js
          - core/server/ghost-server.ts
        tests:
          - test/unit/server/adapters/cache/adapter-cache-memory-ttl.test.js
          - test/unit/server/adapters/lib/redis/adapter-cache-redis.test.js
      failures:
        summary: >-
          Source-only root behavior: dependency/adapter/listener-registration errors reject
          init before service publication. defineService allows a later explicit retry;
          no rollback disposes the borrowed adapter. Registration is the final acquisition.
          A synchronous reset error escapes event emission and an async reset rejection is
          not observed by the listener. The pipeline propagates key/query/cache errors unless
          the adapter swallows them; a truthy cache hit bypasses query/serialization/permission
          stages. Redis logs/swallowing differs by operation: loader rejection can return
          undefined, causing the pipeline to run its response loader again; reset errors are
          logged and swallowed. Fake-store adapter units assert selected failures, not this
          combined request path. Content HTTP asserts 400 for missing public identifiers;
          gift units assert missing-key IncorrectUsage and mismatched-post 403 with no member
          mutation. Service-specific retry, asynchronous invalidation and stale in-flight
          writes are untested.
        sources:
          - core/kernel/define-service.ts
          - core/server/services/posts-public/index.ts
          - core/kernel/adapter-manager/adapter-manager.ts
          - core/server/api/endpoints/posts-public.js
          - core/server/api/endpoints/utils/gift-link-access.ts
          - core/server/adapters/lib/redis/AdapterCacheRedis.js
          - core/server/lib/common/events.js
          - core/boot.js
        tests:
          - test/unit/server/adapters/lib/redis/adapter-cache-redis.test.js
          - test/unit/api/endpoints/utils/gift-link-access.test.ts
          - test/e2e-api/content/posts.test.js
    unverified: []
    blockers:
      - >-
        Keep root-to-controller binding and pipeline adapter identity protected together.
        Existing cold-boot HTTP coverage uses MemoryCache and explicit site.changed, not a
        real Admin mutation, early endpoint import, same-module restart or alternative adapter.
      - >-
        Establish lifecycle ownership before adding reset/re-init: disabled first init is
        permanent, listeners are never removed, manager cache clearing leaves retained users,
        and no root cleanup runs after boot failure or shutdown.
      - >-
        Characterize asynchronous invalidation, concurrent fill/reset and background refresh.
        The event callback does not await reset; an old in-flight MemoryCache response can be
        set after reset, and Redis background refresh can publish through a newer prefix.
        These are source-established race opportunities, not reproduced delivery failures.
      - >-
        Add contract coverage for adapter construction errors, synchronous/async reset failures,
        query/cache failures and Redis's swallowed-loader retry through the real pipeline.
        Bound and drain optional adapter resources without closing a client shared by others.
      - >-
        Keep member/gift cache-key coverage distinct from real cached authorization flows.
        UUID/status/tier tests inject the cache/query and skip validation/permissions; gift-key
        tests stub resolution. Cached revocation, all key dimensions and frontend mutation of
        shared response references have no direct assertions in the inspected cache cohort.
  recommendations:
    kind: directory
    disposition: migrate
    status: audited
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migrated: true
    migration:
      complexity: complex
      modelImpact: fits
      reason: >-
        Migrated: the root defines the service through defineService and its factory composes
        the graph directly, with no wrapper class, and returns only the three controllers its
        consumers read. The foreground graph is complete when init resolves; the two metadata
        refreshes stay detached background work that the factory starts after composition, as
        before. Every construction failure precedes those refreshes, the mentions mapper and
        the event subscription, so a failed attempt leaves nothing to release and a retry
        composes a fresh graph. Nothing releases the subscription, mapper, refresh timers or
        mail transports: defineService has no stop hook and the old wrapper had none either.
      reviewedAt: 8cba94a138a69cbb7e496424591575daab11a529
      sources:
        - core/server/services/recommendations/index.ts
        - core/server/services/recommendations/service/recommendation-service.ts
        - core/server/services/recommendations/service/incoming-recommendation-service.ts
        - core/server/services/mentions/webmention-metadata.js
        - core/boot.js
      dependencies:
        complexity: complex
        reason: >-
          The defineService factory requires config, logging, Sentry, URL helpers, models, mail,
          the external request client, DomainEvents, the mention event class and the mentions,
          oEmbed, settings and staff roots, and passes logging to its inner services and
          repositories; the test-only in-memory repository lives in test support. The factory
          shares the Mentions API, sender and mutable metadata and creates its own mailer and
          settings BREAD service while composing. The mongo-utils key mapping in the base repository is an exact library
          allowance: a pure query transformer with no application instance. Shared mapper
          identity relies on boot initializing Mentions before this root.
        preparation:
          - >-
            Await oEmbed readiness explicitly if it ever gains an initializer; today the shared
            instance is constructed at import, and the mentions mapper installed by this root is
            retained by Mentions metadata for the process lifetime.
          - >-
            Keep Settings reads, staff-recipient lookup and mail delivery as explicit collaborators,
            separating the ready graph from asynchronous publication/metadata refresh.
        reviewedAt: 8cba94a138a69cbb7e496424591575daab11a529
        sources:
          - core/server/services/recommendations/index.ts
          - core/server/services/recommendations/service/recommendation-service.ts
          - core/server/services/recommendations/service/bookshelf-repository.ts
          - core/server/services/recommendations/service/well-known-service.ts
          - core/server/services/mentions/webmention-metadata.js
          - core/boot.js
    entryPoints:
      - recommendations/index.ts
    notes: >-
      Preserve optional composition, database repositories, mentions integration and HTTP
      callers. Record detached startup, settings writes and notification work before changing
      lifecycle ownership; these source risks are not evidence of reproduced failures.
    contracts:
      exports:
        summary: >-
          The TypeScript root default-exports a defineService root whose `.service`, read
          through `.default` by its CommonJS consumers, is a plain object with exactly
          `controller`, `wellknownController` and `incomingRecommendationController`; the
          repositories and inner services are reachable only through those controllers. Reading
          it before init throws IncorrectUsageError and init resolves undefined. When
          services:recommendations:enabled is false, composition still publishes the controllers
          and skips only the two background initializers. The service
          barrel exports constructors and entities, not another application instance. Inner
          methods return promises; controllers serialize plain recommendation values into
          data/meta responses. Controller units with fake services assert selected serialized
          fields, pagination and validation. The root test loads the index and asserts the
          service getter throws before init, that init resolves undefined and publishes only
          the three controllers, and that the three endpoint modules read the service per request, rejecting
          with IncorrectUsageError before initialization.
        sources:
          - core/server/services/recommendations/index.ts
          - core/server/services/recommendations/service/index.ts
          - core/server/services/recommendations/service/recommendation-controller.ts
          - core/server/services/recommendations/service/incoming-recommendation-controller.ts
        tests:
          - test/unit/server/services/recommendations/index.test.ts
          - test/unit/api/endpoints/recommendations.test.ts
          - test/unit/server/services/recommendations/service/recommendation-controller.test.ts
          - test/unit/server/services/recommendations/service/incoming-recommendation-controller.test.ts
      construction:
        summary: >-
          Import declares only the root. The synchronous defineService factory requires the
          shared config, logging, Sentry, URL helpers, models, mail, external request,
          DomainEvents, mention event class and the mentions, oEmbed, settings and staff roots,
          reads the ready Mentions facade for its API and sender, reads model constructors and composes WellknownService, a fresh
          SettingsBREADService and enabler, repositories, metadata/outgoing/incoming services and
          controllers, passing logging to the inner services and repositories. It
          uses BookshelfClickEventRepository for both click and subscription models. Two
          GhostMailers are constructed: one inside SettingsBREADService and one for incoming
          notifications. Metadata borrows the shared oEmbed and external-request objects; the
          renderer retains a getter over the ready Staff service. No other production root construction or deep
          composition was found. Service units use in-memory repositories and fake dependencies;
          booted API/email tests exercise normal composition. The root test runs the real init
          with the Mentions facade replaced and asserts the repositories are bound to the three
          models and shared Sentry, the subscribe repository is the click class, the outgoing
          service holds the published repositories, the mentions sender and the shared URL
          helpers, controllers hold their services, metadata is fetched through the shared
          external request client with oEmbed fallback, incoming emails go through GhostMailer
          to the alert users mapped to email and slug, and the renderer reads the staff API at
          send time. A failure at the first model lookup or at the incoming mailer, the last
          dependency built before the controllers, rejects init, publishes nothing, starts no
          refresh, installs no mapper or subscriber and leaves a retry to compose a complete graph.
        sources:
          - core/server/services/recommendations/index.ts
          - core/server/services/recommendations/service/recommendation-service.ts
          - core/server/services/recommendations/service/incoming-recommendation-service.ts
          - core/server/services/recommendations/service/incoming-recommendation-email-renderer.ts
          - core/server/services/settings/settings-service.js
          - core/server/services/settings/settings-bread-service.js
          - core/server/lib/mail/ghost-mailer.js
        tests:
          - test/unit/server/services/recommendations/index.test.ts
          - test/unit/server/services/recommendations/service/recommendation-service.test.ts
          - test/e2e-api/admin/recommendations.test.js
          - test/e2e-server/services/recommendation-emails.test.js
      consumers:
        summary: >-
          Boot awaits the root. Admin outgoing/incoming and Content/Members endpoints retain
          the root and read the service's controller fields per call; the site router reads the
          well-known controller per request; controllers retain services.
          The theme helper calls the public API, and the frontend serves the generated well-known
          file. The mentions mapper strips its suffix; verified matching MentionCreatedEvents
          call the incoming service composed with the root. Booted HTTP tests assert selected CRUD
          responses, duplicate rejection, count relations, Content pagination/include exclusion,
          and persisted member/anonymous tracking with authentication separation. Booted email
          tests save real mentions and assert two rendered notifications, recommendation-back
          wording and exclusion of ordinary/unverified mentions, using mocked mail transport and
          test-only event settlement. Helper tests replace the API; incoming browse units replace
          its service. The endpoint test asserts each query forwards its frame to the controller
          read at request time and that tracking queries resolve without a body; the root test
          browses incoming recommendations through the published incoming controller with a fake mentions
          API and the published repository. Booted frontend tests assert the well-known file
          reflects repository changes and revalidates its ETag. The real incoming HTTP path and
          well-known readiness at boot remain unproven.
        sources:
          - core/boot.js
          - core/server/api/endpoints/recommendations.js
          - core/server/api/endpoints/recommendations-public.js
          - core/server/api/endpoints/incoming-recommendations.js
          - core/server/api/endpoints/index.js
          - core/server/web/api/endpoints/admin/routes.js
          - core/server/web/api/endpoints/content/routes.js
          - core/server/web/members/app.js
          - core/frontend/helpers/recommendations.js
          - core/frontend/web/routers/serve-public-file.js
          - core/server/services/recommendations/index.ts
          - core/server/services/recommendations/recommendation-url.ts
          - core/server/services/mentions/bookshelf-mention-repository.js
        tests:
          - test/e2e-api/admin/recommendations.test.js
          - test/e2e-api/content/recommendations.test.js
          - test/e2e-api/members/recommendations.test.js
          - test/e2e-frontend/recommendations-well-known.test.js
          - test/e2e-server/services/recommendation-emails.test.js
          - test/unit/api/endpoints/recommendations.test.ts
          - test/unit/server/services/recommendations/index.test.ts
          - test/unit/frontend/helpers/recommendations.test.ts
          - test/unit/server/services/recommendations/service/incoming-recommendation-controller.test.ts
      startup:
        summary: >-
          Boot awaits the Mentions root before the Promise.all group that initializes
          recommendations and staff, so the Mentions facade is ready when the defineService
          factory runs synchronously inside init. The factory throws if Mentions is read before
          it is ready, composes its graph, starts both async refreshes without awaiting them,
          then installs mapper/subscriber and resolves with the ready controllers. Outgoing refresh
          init checks recommendations_enabled before scheduling metadata refresh; incoming init
          schedules revalidation. Direct inner-service units assert refresh invocation after
          advancing a day, outgoing production/development enablement and test/disabled
          suppression. The root test asserts init rejects without publishing anything while
          Mentions is not ready and succeeds once it is, starts both inner initializers on the
          ready services without awaiting them unless config is explicitly false, logs the
          disabled case, and logs an inner initializer rejection while still resolving. The boot
          harness asserts recommendations init waits for mentions init to finish, and that its
          rejection fails initServices before job registration. Exact jitter, completion before HTTP readiness
          and behavior when startup file/DB work fails remain source-only.
        sources:
          - core/boot.js
          - core/server/services/mentions/index.ts
          - core/server/services/recommendations/index.ts
          - core/server/services/recommendations/service/recommendation-service.ts
          - core/server/services/recommendations/service/incoming-recommendation-service.ts
        tests:
          - test/unit/boot/init-services.test.ts
          - test/unit/server/services/recommendations/index.test.ts
          - test/unit/server/services/recommendations/service/recommendation-service.test.ts
          - test/unit/server/services/recommendations/service/incoming-recommendation-service.test.ts
      lifetime:
        summary: >-
          defineService retains the first ready controllers across repeated init and discards a
          failed attempt, whose construction stops before the refreshes, mapper and subscription are
          installed. Disabling later only changes the next factory run; it does not remove the
          retained graph or stop work. Controllers, incoming service, outgoing service and the event
          callback retain their collaborators, while the renderer rereads the Staff service through
          a getter. Inner init methods have no repeat guard, and their timers are not retained. The
          root test asserts repeated init retains the controllers and installs the mapper,
          subscriber and inner initializers once, that a construction failure rejects init,
          publishes nothing and lets a retry compose a complete graph once, and that the event
          callback emails through the published incoming service. Timer retention and dependency
          replacement in retained controllers remain source-only. There is no reset or shutdown: the
          DomainEvents subscription, mentions mapper and detached refreshes are never released, as
          before the wrapper was removed.
        sources:
          - core/server/services/recommendations/index.ts
          - core/server/services/recommendations/service/recommendation-service.ts
          - core/server/services/recommendations/service/incoming-recommendation-service.ts
          - core/server/services/recommendations/service/incoming-recommendation-email-renderer.ts
          - core/server/services/mentions/webmention-metadata.js
        tests:
          - test/unit/server/services/recommendations/index.test.ts
      configuration:
        summary: >-
          Explicit false for services:recommendations:enabled, read from the injected config
          while composing, skips only the background refreshes; repositories, services and
          controllers are still published. Model/service references and both mail transports
          are captured while composing;
          settings BREAD reads current cache synchronously, URL generation uses retained helpers,
          and notification recipients/settings/sender are read during work. Outgoing refresh is
          enabled by setting string true outside test environments after a 2–7 minute delay;
          incoming refresh excludes test and development with a 15–315 second delay. Metadata
          requests default to a 5-second request timeout and retry limit 3, then fall back to
          oEmbed mention fetching without forwarding those options. Nock metadata units use raw
          got, not production request-external. Mail integration asserts recommendation sender
          addresses under selected config overrides with mocked sendMail; it does not prove
          transport reconfiguration. Immutable config replacement does not update captured values.
          The root test asserts the enabler reads the current setting synchronously on every
          call and stringifies it, that only an explicit false config value skips the inner
          initializers, and that metadata requests carry the 5-second timeout and retry limit 3
          to the shared request client.
        sources:
          - core/server/services/recommendations/index.ts
          - core/server/services/recommendations/recommendation-enabler-service.js
          - core/server/services/recommendations/service/recommendation-service.ts
          - core/server/services/recommendations/service/incoming-recommendation-service.ts
          - core/server/services/recommendations/service/recommendation-metadata-service.ts
          - core/server/services/recommendations/service/well-known-service.ts
          - core/server/services/settings/settings-bread-service.js
          - core/server/data/schema/default-settings/default-settings.json
          - core/server/lib/mail/ghost-mailer.js
          - core/server/lib/request-external.js
        tests:
          - test/unit/server/services/recommendations/index.test.ts
          - test/unit/server/services/recommendations/service/recommendation-service.test.ts
          - test/unit/server/services/recommendations/service/recommendation-metadata-service.test.ts
          - test/integration/services/q-email-addresses.test.js
      resources:
        summary: >-
          Root-owned startup timers, a DomainEvents subscription, a mentions mapper and two
          mail transports have no root cleanup/drain. Repositories borrow shared models/DB;
          HTTP requests borrow shared keep-alive agents and oEmbed, whose mention fetch skips
          thumbnail storage. Outgoing webmentions run detached from mutations; incoming emails
          await recipients sequentially inside an event handler that the publisher does not await.
          WellknownService creates directories and overwrites one file without serialization or
          atomic rename. Its unit asserts actual JSON file content, not concurrent writes. Email
          tests use DomainEvents.allSettled, whose tracking is test-only. Helper timeout tests
          prove fallback HTML/logging with an injected API, not cancellation of underlying work.
          The root test asserts the mapper strips only the well-known suffix, preserves the
          query string and leaves its input unchanged. No test establishes production shutdown
          settlement or transport/socket disposal.
        sources:
          - core/server/services/recommendations/index.ts
          - core/server/services/recommendations/recommendation-url.ts
          - core/server/services/recommendations/service/recommendation-service.ts
          - core/server/services/recommendations/service/incoming-recommendation-service.ts
          - core/server/services/recommendations/service/well-known-service.ts
          - core/server/services/settings/settings-bread-service.js
          - core/server/services/mentions/webmention-metadata.js
          - core/server/services/mentions/bookshelf-mention-repository.js
          - core/server/services/mentions/mention-sending-service.js
          - core/server/services/oembed/oembed-service.js
          - core/server/lib/mail/ghost-mailer.js
          - core/server/lib/request-external.js
          - core/frontend/helpers/recommendations.js
          - package.json
        tests:
          - test/unit/server/services/recommendations/index.test.ts
          - test/unit/server/services/recommendations/service/well-known-service.test.ts
          - test/unit/frontend/helpers/recommendations.test.ts
          - test/e2e-server/services/recommendation-emails.test.js
      failures:
        summary: >-
          Composition errors reject init and reach boot; detached inner-init/timer failures are
          logged. CRUD has no transaction spanning DB writes, well-known output, settings and
          mentions, so a later rejection can follow persistence. Enabler setSetting drops the
          BREAD edit promise; awaiting it does not await persistence or propagate its rejection.
          Existing-recommendation check starts metadata refresh without awaiting it before save.
          Other metadata failures log/fall back; malformed model mapping logs/reports then omits
          the row. Incoming conversion failure omits a recommendation; recipient/render/send
          rejection stops that handler, whose DomainEvents boundary logs it. Units assert selected
          fallback values, mapping failure/null and non-propagation of a rejected mention sender
          using fakes; timer rejection cases only assert invocation. The root test asserts
          setSetting resolves before the settings edit settles, that an inner initializer
          rejection is logged rather than thrown, and that a construction failure rejects init
          and is retried from scratch; the service unit asserts checkRecommendation saves and
          returns the stored values before its metadata refresh settles. They do not prove
          rollback, ordered persistence or notification retry.
        sources:
          - core/server/services/recommendations/index.ts
          - core/server/services/recommendations/recommendation-enabler-service.js
          - core/server/services/recommendations/service/recommendation-service.ts
          - core/server/services/recommendations/service/incoming-recommendation-service.ts
          - core/server/services/recommendations/service/recommendation-metadata-service.ts
          - core/server/services/recommendations/service/bookshelf-repository.ts
          - core/server/services/recommendations/service/bookshelf-recommendation-repository.ts
          - core/server/services/settings/settings-bread-service.js
          - package.json
        tests:
          - test/unit/server/services/recommendations/index.test.ts
          - test/unit/server/services/recommendations/service/recommendation-service.test.ts
          - test/unit/server/services/recommendations/service/incoming-recommendation-service.test.ts
          - test/unit/server/services/recommendations/service/recommendation-metadata-service.test.ts
          - test/unit/server/services/recommendations/service/bookshelf-recommendation-repository.test.ts
          - test/e2e-api/admin/recommendations.test.js
    unverified: []
    blockers:
      - >-
        Boot still does not await the initial metadata and revalidation refreshes, which the
        root starts as detached work; existing HTTP success does not establish their
        completion.
      - >-
        Timers, the mentions mapper, the event subscription and both mail transports have no
        teardown, cancellation or production event drain; defineService has no stop hook, so
        the retained graph keeps them for the process lifetime. A shutdown owner must release
        the subscription and refreshes before the root can support stop or restart.
      - >-
        The enabler's synchronous settings read boundary and its dropped edit promise are
        characterized in the root test against the real BREAD service with a stubbed edit.
        Successful mutation responses still do not prove settings persistence completed before
        return.
      - >-
        checkRecommendation's unawaited metadata refresh is characterized with a pending fetch
        in the service unit. Partial DB/file/settings side effects of held operations at the
        boundary being migrated are still not covered.
      - >-
        Incoming browse is covered through the published incoming controller with a fake mentions API and
        the well-known file through a booted frontend test; the real incoming HTTP path,
        well-known readiness at boot, live disable and captured config replacement still lack
        targeted coverage. The helper test titled for a disabled setting actually sets it true;
        metadata units inject raw got and do not establish production retries, DNS guards,
        shared socket ownership or timeout completion.
  remote-flags:
    kind: directory
    disposition: migrate
    futureHome: core/shared/labs
    status: audited
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: complex
      modelImpact: may-change
      reason: >-
        The root deliberately supports inert init, later activation, and stop followed by a
        fresh instance; it also returns before the first fetch finishes. defineService
        retains the first successful instance forever. Decide whether a permanent facade
        owns replaceable pollers or the lifecycle model must grow, including protection
        against an old in-flight refresh writing after stop.
      reviewedAt: f276513ba32b7114fd5cc4f7b02d3f82c0c31e3e
      sources:
        - core/server/services/remote-flags/index.ts
        - core/server/services/remote-flags/remote-flags-service.ts
        - core/shared/labs-flag-overrides.ts
        - core/boot.js
      dependencies:
        complexity: straightforward
        reason: >-
          The root already supplies request, override replacement, site identity and polling
          configuration. The implementation's only remaining application import is logging; crypto
          and Zod in flag resolution are allowed. Disabled startup and poller replacement remain
          lifecycle questions, not dependency blockers.
        preparation:
          - >-
            Inject logging alongside the existing request and override-sink dependencies, preserving
            live override publication and the root's configuration timing.
        reviewedAt: 44ad405bff4a8700af2ed51f833ebc08d25dddff
        sources:
          - core/server/services/remote-flags/index.ts
          - core/server/services/remote-flags/remote-flags-service.ts
          - core/server/services/remote-flags/resolve.ts
          - core/shared/labs-flag-overrides.ts
          - core/boot.js
    entryPoints:
      - remote-flags/index.ts
    notes: >-
      Suggested future home after this migration: alongside core/shared/labs and its flag-override store; remote rollout
      policy is part of the same Ghost-wide feature-flag capability. Boot injects runtime
      dependencies and owns poller startup/shutdown; generic timers do not make the flag policy
      Kernel code. No source has moved. Preserve config-gated background polling, stable rollout
      buckets and labs precedence. Late startup, timer bounds and writes from stopped instances
      remain explicit blockers.
    contracts:
      exports:
        summary: >-
          Named TypeScript exports are synchronous init(config), stop and getInstance; init
          and getInstance return the current RemoteFlagsService or null. The deep implementation
          exports a class with public configuration/state and start, stop and refresh methods;
          resolve.ts separately exports the pure resolver and bucketFor helper.
        sources:
          - core/server/services/remote-flags/index.ts
          - core/server/services/remote-flags/remote-flags-service.ts
          - core/server/services/remote-flags/resolve.ts
        tests:
          - test/unit/server/services/remote-flags/index.test.js
          - test/unit/server/services/remote-flags/resolve.test.js
      construction:
        summary: >-
          Import starts with a null instance but loads the shared request helper and override
          store even when disabled. First enabled init with a parseable URL constructs the
          service, assigns it, starts an immediate refresh without awaiting it and returns it.
          Later init returns the instance before reading config. The root is the only runtime
          construction path found; implementation tests construct the class directly.
        sources:
          - core/server/services/remote-flags/index.ts
          - core/server/services/remote-flags/remote-flags-service.ts
        tests:
          - test/unit/server/services/remote-flags/index.test.js
      consumers:
        summary: >-
          Boot is the only runtime root caller; no production stop/getInstance or deep helper
          caller was found. Accepted manifests replace the shared override store, whose runtime
          reader is labs. Effective precedence is local config over remote over GA over DB,
          with members recomputed afterwards. Labs gates, settings/public-config responses,
          theme validation and template options consume those effective values without
          importing the poller. Tests prove resolver/store/labs behavior, not those HTTP paths.
        sources:
          - core/boot.js
          - core/server/services/remote-flags/index.ts
          - core/shared/labs-flag-overrides.ts
          - core/shared/labs.js
          - core/server/services/settings/settings-bread-service.js
          - core/server/api/endpoints/settings-public.js
          - core/server/api/endpoints/utils/public-config/config.js
          - core/server/services/themes/validate.js
          - core/frontend/services/theme-engine/middleware/update-global-template-options.js
        tests:
          - test/unit/server/services/remote-flags/index.test.js
          - test/unit/shared/labs.test.js
          - test/unit/shared/labs-flag-overrides.test.ts
      startup:
        summary: >-
          Boot calls init late in detached background initialization, after readiness and
          settings setup; test environments skip this phase. An earlier uncaught background
          failure, including activitypub.init rejection, prevents the call. No startup path
          waits for a remote manifest before using flags. The root itself has no environment
          gate and works without a site UUID, applying full overrides while skipping ramps.
          The pending-refresh root test establishes nonblocking init, not boot readiness.
        sources:
          - core/boot.js
          - core/server/services/remote-flags/index.ts
          - core/server/services/remote-flags/resolve.ts
        tests:
          - test/unit/server/services/remote-flags/index.test.js
          - test/unit/server/services/remote-flags/resolve.test.js
      lifetime:
        summary: >-
          One loaded root retains the instance until stop clears it; disabling config does
          not stop it. Stop keeps applied overrides, while a new init resets ETag/change state
          and captures new config. Same-instance start is guarded; overlapping refresh calls
          skip the extra fetch and return immediately rather than join pending completion.
          Direct refresh remains callable after stop, and an old in-flight fetch can overwrite
          a replacement instance's results. The shared store copies replacements and reads.
        sources:
          - core/server/services/remote-flags/index.ts
          - core/server/services/remote-flags/remote-flags-service.ts
          - core/shared/labs-flag-overrides.ts
        tests:
          - test/unit/server/services/remote-flags/index.test.js
          - test/unit/server/services/remote-flags/remote-flags-service.test.js
          - test/unit/shared/labs-flag-overrides.test.ts
      configuration:
        summary: >-
          Enabled must be exactly true; URL, site UUID and poll interval are captured at init.
          Finite numeric intervals of at least 60 seconds are accepted with no upper bound;
          invalid supplied intervals warn and use five minutes. The class adds up to one
          minute of jitter and does not enforce the root's interval validation. Resolution
          accepts arbitrary safe flag names, skips malformed entries, clamps percent and uses
          stable MD5 flag/site buckets for partial ramps. Missing, empty or non-string site identifiers skip ramps only; any other string
          is bucketed. Invalid top-level JSON values resolve to an empty map, replacing old overrides.
        sources:
          - core/server/services/remote-flags/index.ts
          - core/server/services/remote-flags/remote-flags-service.ts
          - core/server/services/remote-flags/resolve.ts
          - core/shared/config/defaults.json
        tests:
          - test/unit/server/services/remote-flags/index.test.js
          - test/unit/server/services/remote-flags/resolve.test.js
      resources:
        summary: >-
          Start refreshes immediately and schedules an unref'ed timeout after completion;
          each timeout refreshes then schedules the next. Stop clears the timer and prevents
          rescheduling, but neither cancels nor awaits requests. GETs use a ten-second timeout,
          no redirects/retries and nonthrowing HTTP statuses through the shared request helper,
          which owns DNS/cache maintenance resources. No database connection or subscription
          is acquired, and boot registers no cleanup. Tests cover settled scheduling with fake
          timers, not in-flight stop or real transport disposal.
        sources:
          - core/server/services/remote-flags/remote-flags-service.ts
          - core/server/services/remote-flags/index.ts
          - core/boot.js
          - core/server/ghost-server.ts
          - package.json
        tests:
          - test/unit/server/services/remote-flags/remote-flags-service.test.js
      failures:
        summary: >-
          Network failures, bad statuses and invalid JSON warn and retain prior overrides;
          304 neither reapplies nor logs, while 404 clears overrides and ETag. Accepted responses
          reapply each time but log only changed resolved maps; a 2xx ETag commits after apply
          succeeds. Apply failure is caught without rollback of any already-written state.
          Root config failures can reject detached background startup. Logger failures can
          reject refresh; scheduler failures reject start or the async timeout callback. Root
          init and timers do not observe these promises, allowing unhandled rejections with
          the instance retained and polling stopped. Normal fetch/apply failures are covered with
          fake requests; these exceptional lifecycle paths are not.
        sources:
          - core/server/services/remote-flags/index.ts
          - core/server/services/remote-flags/remote-flags-service.ts
          - core/boot.js
        tests:
          - test/unit/server/services/remote-flags/index.test.js
          - test/unit/server/services/remote-flags/remote-flags-service.test.js
    unverified: []
    blockers:
      - >-
        Define cleanup and in-flight ownership. Stop returns before pending refresh completes,
        and an old instance can replace newer overrides after stop/re-init. Test this race,
        persistence of stale state through replacement-instance 304 responses, completion
        semantics for overlapping refresh/start calls, and real server shutdown;
        existing fake-timer tests only stop after a settled poll.
      - >-
        Bound the configured interval plus jitter to Node's timer range. Current validation
        accepts delays above 2147483647ms, which Node reduces to 1ms, defeating the minimum
        polling interval. Existing tests cover the lower floor, not overflow or jitter bounds.
      - >-
        Pin background boot ordering and failure behavior, including earlier activitypub
        failure and flag consumers before the first manifest arrives. Root tests mostly stub
        start; the real-start test stubs refresh. No current test connects boot, the real
        request helper and the shared store or verifies cleanup registration.
      - >-
        Add real-client contracts for timeout/no retries/no redirects, parseable but unsupported
        URLs and request disposal. Cover logger/scheduler failure and resulting retained state.
        The no-ETag test never seeds a previous ETag; add an actual old-tag-to-no-tag transition,
        404/apply failure and malformed-manifest replacement before relying on those contracts.
  route-settings:
    kind: directory
    disposition: review
    status: audited
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: complex
      modelImpact: may-change
      reason: >-
        The same instance becomes storage-ready during early boot, then routing-ready after
        applications exist; uploads later reload the shared frontend/URL graph. Decide what
        a ready instance promises and preserve deferred imports that avoid an API/bridge
        cycle. A stable facade with explicit start/reload operations may fit, but folding
        both stages into one initializer would change boot dependencies and failure
        behavior.
      reviewedAt: f276513ba32b7114fd5cc4f7b02d3f82c0c31e3e
      sources:
        - core/server/services/route-settings/index.js
        - core/server/services/route-settings/dynamic-routing-service.js
        - core/boot.js
        - core/bridge.js
        - core/server/api/endpoints/settings.js
      dependencies:
        complexity: complex
        reason: >-
          Storage is configured before routing exists. Upload later requires URL and bridge, with an
          explicit deferred import to avoid the bridge/API/settings cycle. Parsers are also shared
          directly with storage adapters. Root-only wiring needs a late frontend-reload boundary and
          independent parsing support, rather than eagerly resolving the full graph.
        preparation:
          - >-
            Inject a deferred reload operation and the URL facade while retaining separate storage
            configuration and router start.
          - >-
            Move adapter-shared YAML parsing into a capability library, then inject logging/debug
            and parsing into the running service without routing adapters back through its root.
        reviewedAt: 44ad405bff4a8700af2ed51f833ebc08d25dddff
        sources:
          - core/server/services/route-settings/index.js
          - core/server/services/route-settings/dynamic-routing-service.js
          - core/server/services/route-settings/route-settings-parser.ts
          - core/server/services/route-settings/yaml-parser.js
          - core/server/adapters/route-settings/FileStore.ts
          - core/server/adapters/route-settings/S3RouteSettingsStore.ts
          - core/bridge.js
          - core/boot.js
    entryPoints:
      - route-settings/index.js
    notes: >-
      Ownership needs review before an in-place initializer migration or relocation. Routing
      activation has an existing owner, but this root also owns configured storage, API operations
      and parsing used by server adapters. A whole-root move under frontend would violate
      server/frontend dependency boundaries. Decide the backend-safe split first; no destination
      or source move is approved. Preserve separate storage configuration and router start,
      verbatim YAML round trips, live upload reload and adapters' independent parser imports.
      Restart, concurrent updates and storage/client cleanup remain unresolved migration
      obligations.
    contracts:
      exports:
        summary: >-
          CommonJS exports init, an eager service getter, a loadRouteSettings getter and api
          upload/download getters. Each method getter creates a fresh function bound to the
          same service; service methods otherwise require their receiver. Init/start/upload
          resolve undefined; load returns the parsed model and download returns yamlSource.
          Deep exports include DynamicRoutingService, YAML parsing, domain parsing/serialization
          and validation helpers. Parser tests pin domain shapes, ordering, coercions and
          round trips; no focused assertion pins the root getters or their binding/identity.
        sources:
          - core/server/services/route-settings/index.js
          - core/server/services/route-settings/dynamic-routing-service.js
          - core/server/services/route-settings/yaml-parser.js
          - core/server/services/route-settings/route-settings-parser.ts
          - core/server/services/route-settings/validation-errors.ts
        tests:
          - test/unit/server/services/route-settings/dynamic-routing-service.test.ts
          - test/unit/server/services/route-settings/route-settings-parser.test.ts
          - test/unit/server/services/route-settings/validate-route-settings.test.ts
      construction:
        summary: >-
          Import constructs one DynamicRoutingService with null store/routerManager/urlService.
          Async init has no await or guard: each call gets the adapter-manager route-settings
          adapter and synchronously configures that service. The adapter manager caches by
          name and selected class; FileStore captures paths, S3RouteSettingsStore captures
          options and constructs an S3Client unless injected. No separate production
          DynamicRoutingService construction was found. Both adapters import parsers directly
          without loading the root. Wiring tests assert selected adapter type/defaults and
          bad configuration; service tests construct the deep class, not the root.
        sources:
          - core/server/services/route-settings/index.js
          - core/server/services/route-settings/dynamic-routing-service.js
          - core/kernel/adapter-manager/adapter-manager.ts
          - core/server/services/adapter-manager/base-classes.ts
          - core/server/adapters/route-settings/FileStore.ts
          - core/server/adapters/route-settings/S3RouteSettingsStore.ts
        tests:
          - test/unit/server/services/adapter-manager/route-settings-wiring.test.ts
          - test/unit/server/services/route-settings/dynamic-routing-service.test.ts
          - test/integration/services/route-settings/dynamic-routing-service.test.ts
      consumers:
        summary: >-
          Boot calls root init and later service.start. Admin settings retains the root and
          reads api getters per upload/download; upload reads the HTTP temporary file before
          delegating. Exports retains the root through an exportRoutesYaml callback. Upload
          passes the implementation to bridge.reloadFrontend, which reads it before replacing
          frontend routing and restarting apps. RouterManager consumes the domain arrays/map
          and registers URL-service routes; adapters independently consume parser helpers.
          Admin file tests assert statuses/headers, not downloaded YAML bytes. Export HTTP
          tests assert routes.yaml inclusion and re-upload acceptance. Custom frontend tests
          pin changed post/RSS/taxonomy routes; comments tests exercise HTTP upload followed
          by page URL serialization, not all new collection routes.
        sources:
          - core/boot.js
          - core/server/api/endpoints/settings.js
          - core/server/api/endpoints/exports.js
          - core/server/web/api/endpoints/admin/routes.js
          - core/bridge.js
          - core/frontend/web/site.js
          - core/frontend/web/routes.js
          - core/frontend/services/routing/router-manager.js
          - core/server/adapters/route-settings/FileStore.ts
          - core/server/adapters/route-settings/S3RouteSettingsStore.ts
        tests:
          - test/e2e-api/admin/settings-files.test.js
          - test/e2e-api/admin/exports-download.test.js
          - test/e2e-frontend/custom-routes.test.js
          - test/e2e-api/members-comments/comments-lazy-url-parity.test.js
          - test/unit/frontend/services/routing/router-manager.test.js
      startup:
        summary: >-
          Boot validates adapters in initCore, configures route settings before Express apps,
          then always awaits start after apps are constructed and before disabling maintenance.
          Backend-only boot initializes RouterManager with URL service explicitly; frontend
          setup already did so. Start assigns routerManager/urlService before awaiting the
          store read, then synchronously starts the current manager. Init alone allows
          store reads/downloads; upload also requires the bridge/frontend/URL graph. Upload
          imports the global URL service, not the stored start dependency, and defers bridge
          import to avoid its API cycle. Isolated HTTP tests assert configured post/tag URLs
          in backend-only mode, refreshed URLs through the same Content API consumer after
          upload, and configured canonical URLs with only the frontend mounted. VM tests assert
          that initDynamicRouting passes the shared dependencies, awaits start and propagates
          its original error in both modes. These do not pin repeated root init/start, a
          pending real store read or top-level boot awaiting initDynamicRouting.
        sources:
          - core/boot.js
          - core/server/services/route-settings/index.js
          - core/server/services/route-settings/dynamic-routing-service.js
          - core/bridge.js
          - core/frontend/services/routing/router-manager.js
        tests:
          - test/e2e-server/boot-frontend-only.isolated.test.ts
          - test/e2e-server/boot-backend-only.isolated.test.ts
          - test/unit/boot/init-dynamic-routing.test.ts
          - test/e2e-frontend/custom-routes.test.js
          - test/e2e-frontend/helpers/get.test.js
          - test/e2e-frontend/member-stats.test.js
      lifetime:
        summary: >-
          Root and implementation identity survive init; configure replaces the store, start
          replaces routing references and each read fetches fresh persisted content. There is
          no readiness state, shared pending promise, stop or reset. Calling start twice
          without RouterManager.init appends routers rather than clearing the graph. Upload
          validates, awaits replace, then reloads by reading again: updates have no lock/version
          guard, so overlapping writes/reloads need not activate each caller's own content.
          Successful reload resets URL state, resets the router registry and replaces the
          site router. Store contract tests pin input/read mutation isolation and replacement
          rather than merging; bridge deferred tests pin read-before-reset and read failure,
          not root identity, concurrent reloads or successful repeated full routing.
        sources:
          - core/server/services/route-settings/index.js
          - core/server/services/route-settings/dynamic-routing-service.js
          - core/bridge.js
          - core/frontend/web/site.js
          - core/frontend/services/routing/router-manager.js
          - core/frontend/services/routing/registry.js
          - core/server/services/url/lazy-url-service.ts
          - core/server/adapters/route-settings/FileStore.ts
          - core/server/adapters/route-settings/S3RouteSettingsStore.ts
        tests:
          - test/unit/bridge.test.js
          - test/unit/server/adapters/route-settings/file-store.test.ts
          - test/unit/server/adapters/route-settings/helpers/store-contract.ts
          - test/integration/adapters/route-settings/s3-route-settings-store.test.ts
      configuration:
        summary: >-
          Root init takes no config. Adapter-manager copies configured adapter sections and
          fills absent/falsy FileStore paths from content settings/defaultRouteSettings, or
          the S3 default-file path. FileStore is the default; S3 validates bucket/prefix,
          optional tenant/endpoint/region and paired credentials, then captures those values.
          Config is a validated frozen tree; test set/reset and adapter cache clearing do not
          establish production hot reload. Even a fresh lookup reuses a cached same-class
          adapter. Routing changes are persisted data reloads, independent of frozen URL-utils
          site/subdirectory values. Parser preserves YAML source, route order, optional-field
          distinctions and brace permalinks. Config/adapter and parser tests assert these
          transformations; production reconfiguration and old/new retained stores are untested.
        sources:
          - core/server/services/route-settings/index.js
          - core/server/services/route-settings/route-settings-parser.ts
          - core/server/services/adapter-manager/config.ts
          - core/kernel/adapter-manager/adapter-manager.ts
          - core/server/adapters/route-settings/FileStore.ts
          - core/server/adapters/route-settings/S3RouteSettingsStore.ts
          - core/shared/config/defaults.json
          - core/shared/config/overrides.json
          - core/shared/config/SCHEMA.md
          - core/shared/url-utils.ts
          - core/frontend/services/routing/parent-router.js
        tests:
          - test/unit/server/services/adapter-manager/route-settings-wiring.test.ts
          - test/unit/server/services/adapter-manager/config.test.ts
          - test/unit/server/adapters/route-settings/s3-route-settings-store.test.ts
          - test/unit/server/services/route-settings/route-settings-parser.test.ts
          - test/unit/server/services/route-settings/validate-route-settings.test.ts
      resources:
        summary: >-
          The service owns references but no timer, listener, cache or worker. Adapter-manager
          owns cached stores: FileStore performs awaited reads and temporary-file moves;
          S3RouteSettingsStore owns an SDK client/HTTP transport without a destroy hook. Cache
          clearing drops adapters without disposal and this root registers no shutdown task.
          Backups are timestamped to seconds with no retention or write serialization, so
          same-second overwrites reuse a backup path/key. Router/URL state belongs to shared
          routing services; bridge init adds settings listeners on each boot without a matching
          removal here. File/S3 tests pin bytes, backups and mutation isolation; S3 bucket
          suites are availability-gated and their multi-backup case spaces writes apart.
          They do not prove transport release, collision safety or drain on shutdown.
        sources:
          - core/server/services/route-settings/dynamic-routing-service.js
          - core/kernel/adapter-manager/adapter-manager.ts
          - core/server/adapters/route-settings/FileStore.ts
          - core/server/adapters/route-settings/S3RouteSettingsStore.ts
          - core/server/adapters/route-settings/utils.ts
          - core/boot.js
          - core/bridge.js
          - core/frontend/services/routing/registry.js
          - core/server/services/url/lazy-url-service.ts
        tests:
          - test/unit/server/adapters/route-settings/file-store.test.ts
          - test/unit/server/adapters/route-settings/s3-route-settings-store.test.ts
          - test/unit/server/adapters/route-settings/utils.test.ts
          - test/integration/adapters/route-settings/s3-route-settings-store.test.ts
          - test/integration/adapters/route-settings/adapter-manager-wiring.test.ts
      failures:
        summary: >-
          Init rejects adapter resolution/construction failures. Stored validation/YAML errors are
          logged as ROUTE_SETTINGS_VALIDATION_ERROR by load then rethrown unchanged; other reads
          rethrow without that log, and download propagates directly. Upload validates before
          replace; missing storage falls back to bundled defaults, corrupt content does not. File
          writes attempt temporary-file cleanup on failure; S3 request failures become cloneable
          InternalServerErrors retaining stack context. Replace failure prevents reload, but
          reload failure leaves new stored bytes. Bridge read failure preserves the old graph;
          failures after URL reset/router reconstruction have no rollback. Bridge starts app
          reactivation without awaiting its promise; the app loader normally logs and swallows
          activation errors, so successful upload does not prove those apps finished restarting.
          Bridge units stub app init and do not assert this boundary. Boot surfaces fatal start
          failure through shutdown. Deep-class, real-file, parser, stubbed-S3 and bridge tests
          enforce selected failure branches. VM tests propagate a substituted start failure;
          real-root startup failure and full HTTP partial-reload recovery remain untested.
        sources:
          - core/server/services/route-settings/index.js
          - core/server/services/route-settings/dynamic-routing-service.js
          - core/server/services/route-settings/yaml-parser.js
          - core/server/services/route-settings/route-settings-parser.ts
          - core/server/services/route-settings/validation-errors.ts
          - core/server/adapters/route-settings/FileStore.ts
          - core/server/adapters/route-settings/S3RouteSettingsStore.ts
          - core/bridge.js
          - core/frontend/services/apps/index.js
          - core/boot.js
        tests:
          - test/unit/boot/init-dynamic-routing.test.ts
          - test/unit/server/services/route-settings/dynamic-routing-service.test.ts
          - test/integration/services/route-settings/dynamic-routing-service.test.ts
          - test/unit/server/services/route-settings/validate-route-settings.test.ts
          - test/unit/server/services/route-settings/validation-errors.test.ts
          - test/unit/server/adapters/route-settings/file-store.test.ts
          - test/unit/server/adapters/route-settings/s3-route-settings-store.test.ts
          - test/unit/bridge.test.js
    unverified: []
    blockers:
      - >-
        Pin the real root's eager identity, bound accessors, repeated/overlapping init and
        configure/start ordering during pending real-store reads, failed start and same-process
        restart. Preserve the existing backend-only/frontend-only HTTP routing assertions and
        VM start barrier/error checks; they do not establish those remaining root contracts.
      - >-
        Decide and test concurrent upload/reload semantics and failures after persistence or
        URL reset. There is no rollback or serialization, and second-resolution backup names
        collide for rapid replacements; existing multiple-backup coverage avoids that case.
      - >-
        Assign S3-client transport disposal, in-flight storage/reload draining and retained
        adapter ownership, including adapter cache clearing. Repeated boot also needs a
        contract for bridge listeners and router/URL state rather than accumulating state.
      - >-
        Add focused HTTP/root assertions for verbatim download, invalid upload leaving storage
        and active routes unchanged, store-replace failure, and successful changed routing
        after repeated uploads. Existing HTTP file tests mainly assert statuses/headers;
        the comments case does not prove each uploaded collection is activated.
  settings:
    kind: directory
    disposition: split
    status: audited
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: complex
      modelImpact: may-change
      reason: >-
        Each init rebuilds the shared cache and calculated-field listeners while existing
        cache/helper consumers retain their identities; reset and separately constructed
        BREAD instances have other lifetimes. defineService's successful init cannot
        silently replace this repeatable refresh contract. Decide whether refresh/reset
        become service operations and coordinate partial cache publication and early-boot
        consumers.
      reviewedAt: f276513ba32b7114fd5cc4f7b02d3f82c0c31e3e
      sources:
        - core/server/services/settings/settings-service.js
        - core/shared/settings-cache/cache-manager.js
        - core/server/services/settings/settings-bread-service.js
        - core/server/services/settings-helpers/index.js
        - core/boot.js
      dependencies:
        complexity: complex
        reason: >-
          Cache initialization, calculated fields and caller-created BREAD/mail/token graphs are
          composed in settings-service.js. BREAD reaches Stripe later, while models and a migration
          import the site-UUID helper before ordinary service boot. These early utilities and live
          helper/cache dependencies cannot all become reads from one initialized service.
        preparation:
          - >-
            Separate boot-independent site-UUID and settings-value utilities from runtime service
            composition so models and migrations do not depend on a ready service.
          - >-
            Make BREAD construction accept logging, reporting, mail/token and a deferred
            Stripe-connect operation instead of loading collaborators internally.
          - >-
            Wire calculated-field callbacks, cache, settings helpers, limits and email-address
            access with their existing read timing; keep cache refresh/reset distinct from
            construction.
        reviewedAt: 44ad405bff4a8700af2ed51f833ebc08d25dddff
        sources:
          - core/server/services/settings/settings-service.js
          - core/server/services/settings/settings-bread-service.js
          - core/server/services/settings/settings-utils.js
          - core/server/services/settings-helpers/index.js
          - core/server/models/settings.js
          - >-
            core/server/data/migrations/versions/5.124/2025-06-06-23-12-11-create-site-uuid-setting.js
          - core/server/api/endpoints/settings.js
          - core/boot.js
    entryPoints:
      - settings/index.js
      - settings/settings-service.js
    notes: >-
      Planned split and relocation: Ghost-wide settings/cache policy belongs in core/shared
      with settings-cache; API presentation stays with its endpoint. Boot must inject
      models, stores and delivery dependencies rather than introducing Shared imports of
      server code. The cache and caller-owned settings operations retain their distinct
      lifetimes. Settle ownership of protected setting mutations, verification delivery and
      feature-specific side effects separately; these are not API presentation merely
      because endpoints invoke them. No source has moved. Reassess initialization after separating ownership rather than migrating this root in place. Preserve per-boot cache
      rebuilding, synchronous retained readers and separate caller-owned BREAD instances.
      Utility consumers also run before ordinary service boot.
    contracts:
      exports:
        summary: >-
          CommonJS index forwards the settings-service object: init/reset, calculated fields,
          email synchronization, UUID validation, access-code operations, secret helpers and
          a receiver-independent BREAD factory. Init calls sibling methods through this; reset
          synchronously clears the shared cache/listeners. BREAD read succeeds synchronously
          but returns rejected promises for ordinary missing/core-access errors; browse returns
          an array with context and a promise without context. Units await these methods and
          assert selected results/errors, not exact return timing or complete root shape.
        sources:
          - core/server/services/settings/index.js
          - core/server/services/settings/settings-service.js
          - core/server/services/settings/settings-bread-service.js
          - core/server/services/settings/settings-utils.js
        tests:
          - test/unit/server/services/settings/settings-bread-service.test.js
          - test/unit/server/services/settings/settings-service.test.js
      construction:
        summary: >-
          Root import retains model/config/cache/helper dependencies, without initializing the
          cache. Every BREAD factory call creates a SingleUseTokenProvider and BREAD instance;
          its constructor creates GhostMailer and MagicLink. Settings/notifications endpoints
          retain separate instances at lazy module load, and recommendations constructs another
          during its init. Model/default migration use settings-utils directly to avoid full
          service boot; its generated site UUID is module-cached. Tests construct injected BREAD
          classes or boot the endpoints, not factory count, partial construction or mail cleanup.
        sources:
          - core/server/services/settings/settings-service.js
          - core/server/services/settings/settings-bread-service.js
          - core/server/services/settings/settings-utils.js
          - core/server/api/endpoints/settings.js
          - core/server/api/endpoints/notifications.js
          - core/server/api/endpoints/index.js
          - core/server/services/recommendations/index.ts
          - core/server/models/settings.js
          - core/server/data/migrations/versions/5.124/2025-06-06-23-12-11-create-site-uuid-setting.js
        tests:
          - test/unit/server/services/settings/settings-bread-service.test.js
          - test/e2e-api/admin/settings.test.js
      consumers:
        summary: >-
          Boot awaits init and email synchronization before signing keys/i18n. API settings
          uses its retained BREAD for browse/read/edit/verification; notifications persists its
          state through a second BREAD. Recommendations requires synchronous read success, but
          setSetting discards the edit promise, so its completion does not establish persistence.
          Shared settings-cache and bound settings helpers serve many other capabilities without
          calling this root. Real Admin/Content HTTP asserts selected settings, read-only rules,
          email-verification metadata/mail interception and DB-token update outcomes. Retained
          cache/helper refresh units use fake rows; these do not cover every retained BREAD caller.
        sources:
          - core/boot.js
          - core/server/api/endpoints/settings.js
          - core/server/api/endpoints/notifications.js
          - core/server/api/endpoints/settings-public.js
          - core/server/services/recommendations/recommendation-enabler-service.js
          - core/shared/settings-cache/index.js
          - core/server/services/settings-helpers/settings-helpers.js
        tests:
          - test/unit/server/services/settings/repeated-init.test.ts
          - test/e2e-api/admin/settings.test.js
          - test/e2e-api/content/settings.test.js
      startup:
        summary: >-
          Every init obtains cache:settings, populates defaults, enforces the public-site limit,
          reads all rows and overrides, rebuilds the shared cache/calculated fields, then validates
          site_uuid. Real populateDefaults destroys and reinitializes the shared Knex pool before
          reading/inserting defaults. There is no init guard or concurrent-call serialization.
          Cache get tolerates pre-init with undefined; getAll and BREAD operations can fail early.
          Repeated-init units assert retained values/call counts with defaults/DB/adapter stubs;
          enforcement units assert selected generated/internal writes, not real pool or boot ordering.
        sources:
          - core/server/services/settings/settings-service.js
          - core/server/models/settings.js
          - core/shared/settings-cache/cache-manager.js
          - core/boot.js
          - core/server/services/settings/private-site-access-code.js
        tests:
          - test/unit/server/services/settings/repeated-init.test.ts
          - test/unit/server/services/settings/settings-service.test.js
          - test/e2e-api/admin/settings.test.js
      lifetime:
        summary: >-
          Shared CacheManager and bound helper identities survive re-init, while cache rows,
          overrides and calculated-field objects are replaced. Init publishes the new store
          before reset/population; reset clears that store and prior exact event callbacks, not
          a replaced old adapter. Reads with resolve:false expose cached objects; BREAD slack
          and labs reads mutate those objects. Calculated updates generate new IDs/timestamps.
          Units prove retained cache/helper refresh, removed rows after re-init, stable listener
          counts and calculated-handler cleanup, not overlapping init, raw-reference mutation,
          real pool restart or disposal. Root has no shutdown registration.
        sources:
          - core/server/services/settings/settings-service.js
          - core/shared/settings-cache/index.js
          - core/shared/settings-cache/cache-manager.js
          - core/server/services/settings/settings-bread-service.js
          - core/server/ghost-server.ts
        tests:
          - test/unit/server/services/settings/repeated-init.test.ts
          - test/unit/shared/settings-cache.test.js
      configuration:
        summary: >-
          Each init copies host overrides and forces transistor false only when configured custom
          integrations are strictly disabled. Cached overrides apply on read and mark rows read-only;
          changing test config alone does not refresh them. Calculated functions bind helpers that
          read current cache/config, while shared URL utilities retain frozen base URLs. BREAD
          consults current labs/limits/email-address service; its mailer retains construction-time
          transport. Verification omits from; GhostMailer delegates default sender selection to
          the email-address policy at send time. Tests pin selected override refresh/removal, parsing,
          managed/self-hosted verification and masked/read-only HTTP responses, not refusal of
          underlying override-row writes. Production config hot reload is not established.
        sources:
          - core/server/services/settings/settings-service.js
          - core/shared/settings-cache/cache-manager.js
          - core/server/services/settings/settings-bread-service.js
          - core/server/services/settings-helpers/settings-helpers.js
          - core/server/services/email-address/email-address-service.ts
          - core/server/lib/mail/ghost-mailer.js
          - core/shared/url-utils.ts
          - core/shared/config/SCHEMA.md
        tests:
          - test/unit/server/services/settings/repeated-init.test.ts
          - test/unit/server/services/settings/settings-service.test.js
          - test/unit/shared/settings-cache.test.js
          - test/e2e-api/admin/settings.test.js
      resources:
        summary: >-
          Cache initialization owns common model/calculated subscriptions and borrows the manager
          adapter; its get/set/reset/keys usage is synchronous, including unawaited reset. Defaults
          initialization manages the shared Knex pool, not an isolated settings connection. Each
          BREAD owns mail/token/link helpers without stop/drain; token rows are cleaned by members
          jobs when they find rows older than 24 hours. Verification creation, mail, token usage and
          setting writes have no shared transaction. Model multi-setting edit runs row operations
          in parallel and has no implicit shared transaction; a caller may supply one. Existing local-memory cleanup and mocked-mail/real-DB HTTP tests
          do not prove alternate-adapter compatibility, in-flight pool/mail drain or atomic edits.
        sources:
          - core/shared/settings-cache/cache-manager.js
          - core/server/services/settings/settings-service.js
          - core/server/services/settings/settings-bread-service.js
          - core/server/models/settings.js
          - core/server/lib/mail/ghost-mailer.js
          - core/server/lib/magic-link/magic-link.js
          - core/server/services/members/single-use-token-provider.js
          - core/server/services/members/jobs/index.js
          - core/server/services/members/jobs/clean-tokens-task.ts
        tests:
          - test/unit/shared/settings-cache.test.js
          - test/unit/server/services/settings/settings-bread-service.test.js
          - test/e2e-api/admin/settings.test.js
      failures:
        summary: >-
          Repeated-init units prove findAll rejection preserves old cache and retry refreshes.
          Later calculated-field/adapter/UUID failures instead leave published partial/new state
          and possibly listeners; UUID units only call validation directly with a stubbed getter.
          Source applies overrides before checking a missing cached row, causing a dereference;
          the unit named for absent override behavior configures no override. BREAD can persist
          ordinary settings before token/mail failure, and token validation commits usage before
          allowed-key checks or setting persistence. Stripe connect notification precedes writes;
          post-write logging/mail errors can reject after effects. Tests assert selected validation,
          intercepted email and token outcomes, not these partial effects.
        sources:
          - core/server/services/settings/settings-service.js
          - core/shared/settings-cache/cache-manager.js
          - core/server/services/settings/settings-bread-service.js
          - core/server/services/members/single-use-token-provider.js
          - core/server/lib/magic-link/magic-link.js
          - core/server/models/settings.js
        tests:
          - test/unit/server/services/settings/repeated-init.test.ts
          - test/unit/server/services/settings/settings-service.test.js
          - test/unit/server/services/settings/settings-bread-service.test.js
          - test/unit/shared/settings-cache.test.js
          - test/e2e-api/admin/settings.test.js
    unverified: []
    blockers:
      - >-
        Cover overlapping init and failures after cache publication, including calculated fields,
        adapter reset/set and UUID validation. The existing read-retry test stubs defaults and
        cannot establish shared Knex pool safety, full restart or old-adapter disposal.
      - >-
        Protect raw-reference and mixed synchronous/promise contracts for retained BREAD callers.
        Characterize slack/labs read mutation, deleted-setting events and invalid override rows;
        the named missing-override unit does not supply the override needed to exercise that branch.
      - >-
        Cover factory/mail ownership and verification composition through an actual emailed token,
        including validity/usage boundaries, concurrency and persistence/mail failures. Current
        token HTTP creates a token directly; it does not prove the full send-to-update round trip.
      - >-
        Establish partial-effect policy for multi-setting edits, Stripe notification, token usage
        and post-write mail/log failures. Recommendations discards its settings edit promise;
        its enable completion and error propagation are not a durability guarantee.
      - >-
        Define synchronous cache-adapter requirements and shutdown ownership for listeners,
        shared pool and per-caller mail resources before adopting a common lifecycle. Preserve
        config snapshots/live reads and actual default-sender fallback without copying the stale name.
  settings-helpers:
    kind: directory
    disposition: split
    status: audited
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: straightforward
      modelImpact: fits
      reason: >-
        This is one composition-only facade over shared cache, limits, config and URL
        helpers, with no owned subscriptions or transport. Initialize the facade before
        settings binds its calculated-field methods, without requiring populated settings at
        construction. Preserve the exposed cache identity and live reads; cache refresh
        stays owned by settings.
      reviewedAt: f276513ba32b7114fd5cc4f7b02d3f82c0c31e3e
      sources:
        - core/server/services/settings-helpers/index.js
        - core/server/services/settings-helpers/settings-helpers.js
        - core/server/services/settings/settings-service.js
        - core/frontend/services/proxy.js
        - core/boot.js
      dependencies:
        complexity: straightforward
        reason: >-
          Config, settings cache, URL utilities, labs and limits already enter through the
          constructor and are read when methods run. Remaining direct dependencies are
          logging/debug, IP detection and the email-address parser. Constructing the facade need not
          read populated settings, which avoids a new readiness cycle with calculated settings.
        preparation:
          - >-
            Inject logging/debug, IP detection and email parsing, and initialize the facade before
            settings binds calculated-field methods while retaining the shared cache reference.
        reviewedAt: 44ad405bff4a8700af2ed51f833ebc08d25dddff
        sources:
          - core/server/services/settings-helpers/index.js
          - core/server/services/settings-helpers/settings-helpers.js
          - core/server/services/settings/settings-service.js
          - core/frontend/services/proxy.js
          - core/server/api/endpoints/themes.js
    entryPoints:
      - settings-helpers/index.js
    notes: >-
      Planned split out of this catch-all root: member signup/unsubscribe/blocklist rules go to
      Members; Stripe key selection to Stripe; donations, Social Web and analytics eligibility to
      their existing feature owners; sender defaults to shared sender policy. Only common settings
      access/calculated-field bindings stay with Shared settings. Boot injects feature-owned
      calculation callbacks so Shared does not import server features. Preserve early evaluation
      without forcing feature services to initialize first. No source has moved. Split before the initializer migration rather than migrating this facade in place; the remaining calculated-field bindings move with the settings split. Preserve the
      shared facade, exposed cache reference and calculated-field bindings. Helper reads, frozen
      URL inputs and caller-owned result caches have different lifetimes.
    contracts:
      exports:
        summary: >-
          CommonJS exports one eager SettingsHelpers instance; the deep module exports its
          constructor. Synchronous instance methods require their receiver and return settings
          decisions, Stripe keys, addresses, domains or signed URLs. Five injected properties
          are public and mutable; the themes endpoint destructures settingsCache directly.
          There is no init/reset/shutdown API. Deep-class tests exercise selected methods,
          not the complete root shape, exposed property identity or detached invocation.
        sources:
          - core/server/services/settings-helpers/index.js
          - core/server/services/settings-helpers/settings-helpers.js
          - core/server/api/endpoints/themes.js
        tests:
          - test/unit/server/services/settings-helpers/settings-helpers.test.js
      construction:
        summary: >-
          The index is the only production constructor found. Import injects shared settings
          cache, URL helpers, config, labs and limitService; construction only stores references.
          Current methods do not use labs. It imports the email-address parser directly, not
          that service's initialized wrapper. Shared dependency imports construct cache/limits
          objects and URL helpers, but this root opens no connection or transport. Deep-class
          tests use injected substitutes; repeated-settings tests load a separate real root.
        sources:
          - core/server/services/settings-helpers/index.js
          - core/server/services/settings-helpers/settings-helpers.js
          - core/shared/settings-cache/index.js
          - core/shared/url-utils.ts
          - core/server/services/limits.ts
          - core/server/data/db/index.js
        tests:
          - test/unit/server/services/settings-helpers/settings-helpers.test.js
          - test/unit/server/services/settings/repeated-init.test.ts
      consumers:
        summary: >-
          Settings uses bound helpers for calculated fields; themes retains the cache property.
          The frontend proxy supplies analytics/Stripe decisions to ghost_head and validation
          keys to unsubscribe. Members uses the facade for config, donation/paid gates, signed
          links and request validation; email rendering, welcome emails, comments, staff, gifts,
          invites and email-address use addresses/links. Posts export reads membership decisions;
          Stripe and machine payments resolve keys. HTTP signed-link tests stub the root key
          method, frontend analytics tests stub the proxy, and email-address wrapper tests stub
          helper results. Real email-address integration exercises this root through managed and
          self-hosted staff/newsletter sending with provider calls mocked; it pins selected
          address/domain composition, not every consumer or retained sender lifecycle.
        sources:
          - core/server/services/settings/settings-service.js
          - core/server/api/endpoints/themes.js
          - core/frontend/services/proxy.js
          - core/frontend/helpers/ghost_head.js
          - core/frontend/services/routing/controllers/unsubscribe.js
          - core/server/services/members/service.js
          - core/server/services/members/api.js
          - core/server/services/members/members-config-provider.js
          - core/server/services/members/members-api/services/member-bread-service.js
          - core/server/services/members/members-api/controllers/router-controller.js
          - core/server/services/members/middleware.js
          - core/server/services/email-service/email-service-wrapper.js
          - core/server/services/email-service/email-renderer.js
          - core/server/services/member-welcome-emails/index.ts
          - core/server/services/comments/index.ts
          - core/server/services/comments/comments-service-emails.js
          - core/server/services/staff/index.ts
          - core/server/services/staff/staff-service-emails.js
          - core/server/services/gifts/index.ts
          - core/server/services/invites/index.ts
          - core/server/services/invites/invites.ts
          - core/server/services/email-address/email-address-service-wrapper.js
          - core/server/services/posts/posts-service-instance.js
          - core/server/services/posts/posts-exporter.js
          - core/server/services/stripe/service.js
          - core/server/services/stripe/config.js
          - core/server/services/machine-payments/index.js
          - core/server/services/machine-payments/stripe/deposit-address-store.ts
          - core/server/services/machine-payments/stripe/payment-recorder.ts
          - core/server/services/machine-payments/adapters/mpp-adapter.ts
        tests:
          - test/e2e-api/members/middleware.test.js
          - test/e2e-frontend/members.test.js
          - test/unit/frontend/helpers/ghost-head.test.js
          - test/unit/server/services/email-address/wrapper.test.ts
          - test/integration/services/q-email-addresses.test.js
      startup:
        summary: >-
          Boot initializes limits before settings; settings fills raw cache rows before invoking
          bound helpers for calculated fields. This root has no readiness check. Before cache
          initialization, missing reads can produce plausible defaults: members enabled is true,
          signup/invite gates false, keys null and validation key undefined; signing with a uuid
          then fails without a key. Construction does not require initialized settings, but methods
          have different prerequisites. Repeated-settings tests pin retained helpers after init
          with fake DB rows, not early method calls or failure partway through calculated fields.
        sources:
          - core/boot.js
          - core/server/services/settings-helpers/settings-helpers.js
          - core/server/services/settings/settings-service.js
          - core/shared/settings-cache/cache-manager.js
          - core/server/services/limits.ts
        tests:
          - test/unit/server/services/settings/repeated-init.test.ts
      lifetime:
        summary: >-
          Module caching retains one facade with no result cache. Settings calculated fields and
          the proxy's analytics function retain bound methods; proxy Stripe/key delegates look up
          methods at call time. Themes retains the original cache property even if it is replaced
          on the facade. Welcome emails cache derived sender options until their own reload/test
          send; Stripe captures key config when configuring, while machine-payment clients check
          current helper keys when resolving a client. Repeated-settings tests prove existing
          facade references see refreshed rows and recover after a failed DB read, not replacement
          of the facade or every downstream cached result.
        sources:
          - core/server/services/settings-helpers/index.js
          - core/server/services/settings-helpers/settings-helpers.js
          - core/server/services/settings/settings-service.js
          - core/frontend/services/proxy.js
          - core/server/api/endpoints/themes.js
          - core/server/services/member-welcome-emails/index.ts
          - core/server/services/stripe/config.js
          - core/server/services/stripe/service.js
          - core/server/services/machine-payments/stripe/deposit-address-store.ts
          - core/server/services/machine-payments/stripe/payment-recorder.ts
        tests:
          - test/unit/server/services/settings/repeated-init.test.ts
          - test/unit/server/services/settings-helpers/settings-helpers.test.js
      configuration:
        summary: >-
          Helpers read settings/config and limit state on each call; the shared URL helper is
          created frozen and returns captured site/subdirectory URLs. Config set/reset in tests
          rebuilds an immutable tree and test setup unfreezes URLs; this is not production hot
          reload. A truthy stripeDirect forces direct keys or null; otherwise selection prefers
          complete connect keys then direct. Email parsing preserves names and falls back to a
          generated domain address; missing support uses the current default. Enabled managed
          email with sendingDomain overrides the site-derived domain; otherwise the site URL
          is normalized with leading www removed. Real email integration pins managed domain/
          localpart expansion and self-hosted default/support outcomes. Unit assertions pin Stripe
          precedence, support fallback changes, HMAC/query options, domain normalization, social
          gates and analytics config/limit gates, with injected cache/limits and selected config.
        sources:
          - core/server/services/settings-helpers/settings-helpers.js
          - core/shared/url-utils.ts
          - core/shared/config/validated.ts
          - core/server/services/limits.ts
          - package.json
        tests:
          - test/unit/server/services/settings-helpers/settings-helpers.test.js
          - test/utils/vitest-setup.ts
          - test/utils/vitest-setup-db.ts
          - test/utils/config-utils.ts
          - test/integration/services/q-email-addresses.test.js
      resources:
        summary: >-
          Helpers own no timer, event subscription, database work or mail/provider client; URL,
          address parsing, HMAC and domain normalization are synchronous. They borrow shared
          cache and limit state. Settings-cache owns calculated-field listeners and reset, while
          mail/Stripe work and retained clients belong to callers. No cleanup or operation drain
          exists on this facade. Repeated-settings tests assert stable listener counts with real
          cache/events and fake DB/adapter input; they do not establish consumer shutdown or
          cleanup after calculated-field failure.
        sources:
          - core/server/services/settings-helpers/settings-helpers.js
          - core/shared/settings-cache/cache-manager.js
          - core/server/services/settings/settings-service.js
          - core/server/services/stripe/service.js
          - core/server/services/machine-payments/stripe/deposit-address-store.ts
          - core/server/services/member-welcome-emails/index.ts
        tests:
          - test/unit/server/services/settings/repeated-init.test.ts
      failures:
        summary: >-
          There is no catch/retry around helper dependencies. Invalid Stripe key type throws
          IncorrectUsageError; invalid URLs, missing signing keys, limit errors and truthy
          nonstring blocklist items can throw synchronously. Nonarray blocklists become empty,
          missing Stripe pairs return null, and an unparsed mail.from warns before fallback.
          Calculated-field exceptions can interrupt cache initialization or synchronous settings
          event delivery without undoing earlier work. Units pin nonarray/empty blocklist fallback
          and selected disabled gates, not these exception paths or partial initialization.
        sources:
          - core/server/services/settings-helpers/settings-helpers.js
          - core/shared/settings-cache/cache-manager.js
          - core/server/lib/common/events.js
          - core/server/services/settings/settings-service.js
        tests:
          - test/unit/server/services/settings-helpers/settings-helpers.test.js
    unverified: []
    blockers:
      - >-
        Pin the root's exported fields, receiver requirements and retained consumers before
        replacing it. Preserve themes' cache reference, bound calculated-field/analytics methods
        and call-time proxy delegates; helper unit tests alone do not cover those identities.
      - >-
        Characterize methods before settings/limits readiness and failed calculated-field init
        or settings-event callbacks, including remaining cache entries/listeners. Existing
        repeated-init failures occur before cache replacement, not during helper evaluation.
      - >-
        Extend real address/domain composition beyond the existing managed/self-hosted sending
        cases to absent/unparseable mail.from and retained sender changes. Add signed-link
        generation-to-verification, Stripe key changes and caller-owned cache contracts. Other
        consumer tests often stub helper methods; distinguish live reads from frozen URLs, calculated settings
        and cached welcome-email senders rather than inferring hot reload from test config edits.
      - >-
        Cover missing/malformed signing keys and URLs, invalid Stripe key type, invalid blocklist
        elements and email parser/fallback edge cases. Social-web coverage's localhost/IP-named
        test only supplies localhost; add IPv4/IPv6 and bracketed-host cases before changing
        that guard. These source risks have not been reproduced by this audit.
  signing-keys:
    kind: directory
    disposition: migrate
    futureHome: core/server/auth/signing-keys
    migrated: true
    extras:
      scheduleCheckJob:
        purpose: Attach the jobs backend and register recurring checks for an active or subsequent key rotation.
        callers:
          - core/boot.js
        timing: Background boot after signing-key readiness and jobs startup; initialization alone does not register jobs.
        dependencies:
          - A ready signing-key instance and the supplied jobs backend.
        failurePolicy: Registration errors propagate to background boot, which logs and continues; a later explicit call can retry.
        destination: A declared domain jobs contribution or shared background-activation interface, preserving the rotation callback.
    status: audited
    auditedAt: 5dae46983fe5423a900214dfc1906f5d14e637d2
    migration:
      complexity: straightforward
      modelImpact: fits
      reason: >-
        Already migrated: the factory constructs one instance and awaits its initial key
        check before publication. Background scheduling has a separate owner, demonstrating
        that ready composition and recurring work need not share a lifecycle hook.
      reviewedAt: f276513ba32b7114fd5cc4f7b02d3f82c0c31e3e
      sources:
        - core/server/services/signing-keys/index.ts
        - core/kernel/define-service.ts
        - core/boot.js
    entryPoints:
      - signing-keys/index.ts
    notes: >-
      Suggested future home after this migration: server/auth/signing-keys; staff/member key purposes, publication/grace
      periods and persisted rotation are authentication policy. Ordinary public-key conversion
      stays in server/lib/public-jwk; Boot owns construction and recurring-check activation, while
      the handler stays auth-owned. Preserve the migrated initializer at its destination; no
      source has moved. Publish only after the initial key check; preserve live key reads and
      purpose separation. Shared initialization guards publication; recurrence, restart and drain
      ownership remain open.
    contracts:
      exports:
        summary: >-
          The default export is a defineService root with init(): Promise<void> and a read-only
          service getter; extras.scheduleCheckJob groups background registration. There are no
          named runtime exports. The provider and purpose
          exports are types only. Reading service before publication throws IncorrectUsageError.
          The implementation exports SigningKeyService and rotation constants;
          CheckSigningKeysJob is a separate class consumed by handler registration. Root tests
          assert pre-init access fails; the full export list is source evidence.
        sources:
          - core/server/services/signing-keys/index.ts
          - core/kernel/define-service.ts
          - core/server/services/signing-keys/signing-key-service.ts
          - core/server/services/signing-keys/check-signing-keys-job.ts
          - core/server/services/jobs-service/register-job-handlers.ts
        tests:
          - test/unit/server/services/signing-keys/index.test.ts
      construction:
        summary: >-
          The root is the only production construction site found. init creates a service
          using the shared settings cache, Settings model, model transaction function,
          logging and a scheduling callback. It awaits check before publishing the service;
          concurrent calls share one pending initialization. Importing the module alone creates
          neither the service nor a recurring job. Root tests verify no key check or published
          instance on import, publication after a held check completes, a shared promise and
          single check for concurrent init, and retry after rejection. Dependency assembly
          remains source evidence; the real rotation callback is exercised with storage doubles.
        sources:
          - core/server/services/signing-keys/index.ts
          - core/kernel/define-service.ts
          - core/server/services/signing-keys/signing-key-service.ts
        tests:
          - test/unit/server/services/signing-keys/index.test.ts
      consumers:
        summary: >-
          Boot initializes the root and later supplies the jobs instance. Identity tokens and
          the staff JWKS app retain staff providers; members API construction retains a
          members provider for signing, verification and public keys. The check-signing-keys
          job handler instead reads the root service getter for each run. Handler tests register
          before root initialization, then verify deferred access, awaiting the check and propagation
          of its original rejection. Existing HTTP tests cover
          both JWKS responses and staff-token verification, not live rotation through those
          retained HTTP consumers.
        sources:
          - core/boot.js
          - core/server/services/identity-tokens/identity-token-service-wrapper.js
          - core/server/web/well-known.js
          - core/server/services/members/api.js
          - core/server/services/members/members-api/services/token-service.js
          - core/server/services/jobs-service/register-job-handlers.ts
        tests:
          - test/e2e-server/well-known.test.ts
          - test/e2e-api/members/well-known.test.js
          - test/legacy/api/admin/identities.test.js
          - test/unit/server/services/jobs-service/register-job-handlers.test.ts
      startup:
        summary: >-
          initCore awaits init after settings and before token/JWKS consumers. check advances
          due rotation and initiates replacement of weak keys. After jobs startup,
          background boot calls extras.scheduleCheckJob, which stores the jobs reference and checks
          rotation state. Registration is skipped in test environments, without jobs, or after
          a prior successful schedule; the guard is set only after registration resolves. Root
          tests cover idle/rotating state, test-environment suppression, registration awaiting,
          sequential deduplication and retry, plus rotation before and after jobs are supplied.
          Boot units hold settings initialization, settings synchronization and signing-key
          initialization pending; they also hold background registration and verify continuation
          after its rejection. Ordering before token/JWKS consumers and after jobs startup
          remains source evidence.
        sources:
          - core/boot.js
          - core/server/services/signing-keys/index.ts
          - core/server/services/signing-keys/signing-key-service.ts
        tests:
          - test/unit/server/services/signing-keys/index.test.ts
          - test/unit/boot/init-core.test.ts
          - test/unit/boot/init-background-services.test.ts
      lifetime:
        summary: >-
          service, jobs and scheduled survive repeated init with no reset/disposal hook.
          forPurpose creates new provider objects whose closures retain the same service and
          read its current settings on each call. Integration tests retain a provider while
          promoting and retiring keys, and race separate class instances; they do not exercise
          root initialization or jobs restart. Root tests assert concurrent init shares one
          pending check and successful later init retains the instance without checking again.
        sources:
          - core/server/services/signing-keys/index.ts
          - core/kernel/define-service.ts
          - core/server/services/signing-keys/signing-key-service.ts
        tests:
          - test/integration/services/signing-keys.test.ts
          - test/unit/server/services/signing-keys/index.test.ts
      configuration:
        summary: >-
          Members and staff use distinct settings keys. Reads use unresolved settings rows
          and their timestamps; promotion/retirement recheck values and age under row locks.
          Keys below 2048 bits trigger rotation; generated replacements use 2048 bits,
          with 48 hours of advance publication and a two-hour old key grace period. JWKS orders active, next, previous; an unknown or missing kid
          falls back to the first published verification key. Recurrence uses randomly chosen
          seconds/minute in a six-field hourly cron, without a caller-supplied timezone. The
          root test pins the cron for controlled random values. Integration tests assert
          rotation transitions using the exported duration constants; they do not independently
          pin the literal delays or default RSA generation. Those details and the absence of
          a timezone option, along with unknown/missing-kid fallback, remain source evidence.
        sources:
          - core/server/services/signing-keys/signing-key-service.ts
          - core/server/services/signing-keys/index.ts
        tests:
          - test/integration/services/signing-keys.test.ts
          - test/unit/server/services/signing-keys/signing-key-service.test.ts
          - test/unit/server/services/signing-keys/index.test.ts
      resources:
        summary: >-
          The service borrows settings/models and opens transactions through the model owner;
          RSA generation is per rotation. The shared public-jwk helper caches public metadata
          promises by PEM digest, evicts failures and exposes no reset/size limit. Recurring
          checks are owned by the supplied jobs backend, not a timer in this root. The root
          has no stop, drain or cancellation hook for checks, rotation or its retained schedule.
          These ownership facts are source evidence. The cited public-key helper tests cover
          derived key data and cache identity, not failed-entry eviction or lifecycle cleanup.
        sources:
          - core/server/services/signing-keys/index.ts
          - core/server/services/signing-keys/signing-key-service.ts
          - core/server/lib/public-jwk.ts
          - core/server/services/jobs-service/jobs-service.ts
        tests:
          - test/unit/server/lib/public-jwk.test.ts
      failures:
        summary: >-
          Initial check failures reject init before publication and abort required boot.
          Signing without an active key throws IncorrectUsageError; malformed settings raise
          InternalServerError and crypto/database errors propagate. Each rotation step has its
          own transaction: no rollback spans the whole check. Background registration errors
          are logged by boot; a rotation callback error rejects after the new key is committed.
          Job check failures are passed to JobsService error handling. Root tests assert
          initial-check rejection without publication and successful retry, plus registration
          rejection and retry. Boot units assert initCore propagates the initial error and
          background boot logs registration failure and continues; handler tests assert the
          original check rejection escapes the registered handler. The class unit tests cover
          malformed settings. Other error paths, including a callback failure after commit,
          remain source evidence.
        sources:
          - core/boot.js
          - core/server/services/signing-keys/index.ts
          - core/kernel/define-service.ts
          - core/server/services/signing-keys/signing-key-service.ts
          - core/server/services/jobs-service/register-job-handlers.ts
          - core/server/services/jobs-service/jobs-service.ts
        tests:
          - test/unit/server/services/signing-keys/signing-key-service.test.ts
          - test/unit/server/services/signing-keys/index.test.ts
          - test/unit/boot/init-core.test.ts
          - test/unit/boot/init-background-services.test.ts
          - test/unit/server/services/jobs-service/register-job-handlers.test.ts
    unverified: []
    blockers:
      - >-
        Characterize retry after a partially completed initial check. Concurrent publication
        and retry after a rejected check are covered with a stubbed check; they do not prove
        recovery when earlier rotation steps committed before a later step failed.
      - >-
        Characterize scheduling failure after a rotation commits, concurrent registration and
        recurrence restoration across jobs restart. The module keeps scheduled after shutdown
        and has no wrapper reset; do not infer custom backend behavior from the default backend.
      - >-
        Verify staff/member token and HTTP JWKS consumers across live rotation, plus missing
        keys, unknown kid fallback and request errors. Empty verification-key lists currently
        cause a native dereference error; no focused assertion covers that case.
      - >-
        Decide ownership/drain of rotations and shared public-key cache lifetime during an
        in-process restart. Existing integration tests use injected keys and class instances,
        not root scheduling, shutdown or crypto-generation failure paths. The public-jwk
        failure test only checks repeated rejection, so it does not prove failed entries
        are evicted rather than returning the same rejected promise.
  slack-ping:
    kind: directory
    disposition: migrate
    status: audited
    auditedAt: ffda65ff64f78a7148f8cbf1d8c717d55bc5a65b
    migrated: true
    migration:
      complexity: straightforward
      modelImpact: fits
      reason: >-
        Migrated with the existing defineService contract: boot awaits readiness, and the
        factory rolls back partially registered listeners before an explicit retry. Stable
        listener identities, live settings checks and detached notifications are preserved.
        Normal shutdown and pending-send ownership remain separate work.
      reviewedAt: ebe8d3df2068fbfccd8a671dc5cc1b0b8265b952
      sources:
        - core/server/services/slack-ping/index.ts
        - core/server/services/slack-ping/slack-ping-service.ts
        - core/kernel/define-service.ts
        - core/boot.js
    entryPoints:
      - slack-ping/index.ts
    notes: >-
      Preserve live Slack settings, publish/test event behavior and request failure handling.
      Failed initialization now rolls back its own subscriptions; normal restart, shutdown
      and shared HTTP-resource ownership remain separate work.
    contracts:
      exports:
        summary: >-
          TypeScript default-exports one defineService root; CommonJS boot reads .default.
          init returns a shared Promise<void>, and the service getter throws IncorrectUsageError
          until initialization succeeds. The named SlackPingService class exposes ping,
          settings/event methods, stable listener references and unsubscribeEvents. The root
          has no reset or shutdown. Root tests cover readiness and repeated/concurrent init;
          the kernel tests cover the generic initializer contract.
        sources:
          - core/server/services/slack-ping/index.ts
          - core/server/services/slack-ping/slack-ping-service.ts
          - core/kernel/define-service.ts
          - core/boot.js
        tests:
          - test/unit/server/services/slack-ping/index.test.ts
          - test/unit/kernel/define-service.test.ts
      construction:
        summary: >-
          First init obtains shared events, settings, URL helpers, blogIcon and request-external,
          constructs SlackPingService and subscribes it before returning the ready instance.
          Initialization and subscription are not gated on a Slack URL. The root test checks
          every wired dependency and listener identity. The root is the only runtime
          SlackPingService constructor found; tests also inject collaborators directly.
          Source-only: importing the image library obtains image cache/storage adapters and
          constructs shared helpers whose lifetime remains outside Slack's factory.
        sources:
          - core/server/services/slack-ping/index.ts
          - core/server/services/slack-ping/slack-ping-service.ts
          - core/server/lib/image/index.js
          - core/server/lib/image/image-utils.js
          - core/server/lib/request-external.js
        tests:
          - test/unit/server/services/slack-ping/index.test.ts
          - test/unit/server/services/slack-ping/slack-ping-service.test.js
      consumers:
        summary: >-
          Boot is the only direct runtime root caller. The shared model emitter invokes
          post.published with the post model; the listener forwards authors/tags and skips
          imports. The authenticated Admin slack/test endpoint emits slack.test, whose listener
          sends a fixed test message. Both listeners discard the ping promise. The isolated
          HTTP suite boots Ghost, publishes a tagged post and calls slack/test through the
          actual emitter and request-external client intercepted at HTTP; both requests finish
          while webhook delivery remains pending. The older API test mocks event emission.
          No CLI, scheduled job or alternate runtime constructor was found in the source search.
        sources:
          - core/boot.js
          - core/server/services/slack-ping/slack-ping-service.ts
          - core/server/models/post.js
          - core/server/models/base/plugins/events.js
          - core/server/api/endpoints/slack.js
          - core/server/web/api/endpoints/admin/routes.js
        tests:
          - test/unit/server/services/slack-ping/index.test.ts
          - test/unit/server/services/slack-ping/slack-ping-service.test.js
          - test/e2e-api/admin/slack.test.js
          - test/e2e-server/slack-ping.isolated.test.ts
      startup:
        summary: >-
          Required-service boot awaits init in Promise.all after settings and dynamic routing
          are initialized and before readiness. Root import does not subscribe. The factory
          installs listeners synchronously when init is called, but callers must await init
          before reading service; initialization does not send a webhook or await delivery.
          Root tests cover import inactivity and the readiness guard. The isolated HTTP suite
          reads the ready instance after boot and verifies that repeated init retains the
          instance handling the test endpoint. Direct class users have no init guard and depend
          on their injected collaborators being ready, as established by source.
        sources:
          - core/boot.js
          - core/kernel/define-service.ts
          - core/server/services/slack-ping/index.ts
          - core/server/services/slack-ping/slack-ping-service.ts
        tests:
          - test/unit/server/services/slack-ping/index.test.ts
          - test/e2e-server/slack-ping.isolated.test.ts
      lifetime:
        summary: >-
          Successful init retains one instance, its dependencies and both bound listener
          identities; repeated/concurrent calls do not duplicate subscriptions. Root tests
          verify retention and single delivery for each event, including after failed-start
          cleanup and retry. Source-only: subscribeEvents removes/re-adds only its own callbacks,
          so it cannot replace another instance's listeners; init does not restore callbacks
          removed elsewhere. Same-process restart retains the root. There is no local queue,
          send deduplication or serialization, and server stop supplies no Slack reset or
          unsubscribe. The new unsubscribeEvents method is called only for factory rollback.
        sources:
          - core/server/services/slack-ping/index.ts
          - core/server/services/slack-ping/slack-ping-service.ts
          - core/kernel/define-service.ts
          - core/server/lib/common/events.js
          - core/boot.js
          - core/server/ghost-server.ts
        tests:
          - test/unit/server/services/slack-ping/index.test.ts
          - test/e2e-server/slack-ping.isolated.test.ts
      configuration:
        summary: >-
          Each ping reads Slack URL, username and site title; blank username falls back to Ghost.
          Root tests change URL/username on a retained instance and assert a disabled test ping
          makes no request. The HTTP suite checks custom title/username/icon, custom excerpt,
          collection-filtered post URLs, author URLs and feature-image URLs through real helpers.
          Class tests cover the fallback username, page/welcome suppression and removal of
          members-only content. Source-only: URL/description work precedes the missing-URL,
          page and default-slug gates; excerpts otherwise strip HTML and keep three sentences.
          request-external captures agent options and test hooks at import, reads environment
          and site URL per request, and configures a 10-second request timeout or 5 seconds
          under its test hook. Slack adds no local timeout or retry policy; the HTTP suite
          uses test hooks and does not establish production DNS, agent or timeout policy.
        sources:
          - core/server/services/slack-ping/slack-ping-service.ts
          - core/server/services/slack-ping/constants.ts
          - core/server/lib/image/blog-icon.js
          - core/server/services/url/lazy-url-service.ts
          - core/server/lib/request-external.js
        tests:
          - test/unit/server/services/slack-ping/index.test.ts
          - test/unit/server/services/slack-ping/slack-ping-service.test.js
          - test/e2e-server/slack-ping.isolated.test.ts
      resources:
        summary: >-
          The service owns two shared-emitter subscriptions and initiates outbound requests.
          If either registration throws, the factory removes only that instance's callbacks
          before rejecting; root tests preserve unrelated listeners and retry without duplicates.
          Root tests exercise overlapping pending sends, and the HTTP suite holds real client
          responses while API/model requests finish. Source-only: no cancellation or drain
          bookkeeping exists. Settings, routing and image helpers are borrowed; image-adapter
          lifetime remains external. request-external uses shared configurable HTTP/HTTPS agents,
          keep-alive by default, DNS checks and a request timeout. No Slack cleanup is registered,
          the client exposes no lifecycle disposal hook, and server stop does not await sends.
        sources:
          - core/server/services/slack-ping/index.ts
          - core/server/services/slack-ping/slack-ping-service.ts
          - core/server/lib/request-external.js
          - core/server/lib/image/index.js
          - core/boot.js
          - core/server/ghost-server.ts
        tests:
          - test/unit/server/services/slack-ping/index.test.ts
          - test/e2e-server/slack-ping.isolated.test.ts
      failures:
        summary: >-
          Failed initialization rejects without publishing an instance and permits a later
          explicit retry. Root tests make either registration throw after adding its listener,
          verify rollback preserves unrelated callbacks, and verify successful retry delivery.
          Request rejection is wrapped in InternalServerError, logged and swallowed: root tests
          check both event paths, error context/help, original error and fulfilled ping promises;
          the HTTP suite checks test-endpoint success followed by a logged delivery failure.
          Synchronous request invocation errors escape both event listeners in root tests.
          Source-only: malformed settings/models, formatting and URL/icon errors likewise occur
          outside the catch and may interrupt later synchronous emitter callbacks, including
          events emitted after transaction commit without undoing persistence. The service has
          no automatic retry or replay, and slack/test success does not report later delivery errors.
        sources:
          - core/server/services/slack-ping/index.ts
          - core/server/services/slack-ping/slack-ping-service.ts
          - core/kernel/define-service.ts
          - core/server/lib/common/events.js
          - core/server/models/base/plugins/events.js
          - core/server/api/endpoints/slack.js
        tests:
          - test/unit/server/services/slack-ping/index.test.ts
          - test/unit/server/services/slack-ping/slack-ping-service.test.js
          - test/e2e-server/slack-ping.isolated.test.ts
    unverified: []
    blockers:
      - >-
        Decide and test listener ownership across restart/replacement and externally cleared
        emitters. Stable callbacks prevent duplication only for one implementation object;
        successful init does not restore externally removed callbacks. Failed-start rollback
        and explicit retry are covered, but there is still no normal shutdown/reset contract.
      - >-
        Characterize shutdown with pending or overlapping sends through the real request helper,
        plus production timeout, cancellation, DNS and shared-agent disposal behavior. The HTTP
        suite verifies pending delivery and logged rejection under test hooks, and root tests
        overlap stubbed requests; neither establishes shutdown draining or production HTTP policy.
        Keep per-request timeout distinct from an overall deadline.
      - >-
        Add synchronous failure contracts for malformed models/settings and throwing URL/icon
        dependencies, including disabled Slack and skipped-post paths. The root tests cover
        synchronous request throws, but not these earlier failures or their effect on later
        callbacks and already-committed model changes.
      - >-
        Complete no-request coverage for every default slug and missing-URL post delivery,
        and live title/icon changes on an already initialized instance. Existing tests cover
        welcome/page suppression, disabling a test notification, changed URL/username, and
        initial custom title/icon values, but not every live-setting and suppression case.
  staff:
    kind: directory
    disposition: migrate
    migrated: true
    status: audited
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: complex
      modelImpact: fits
      reason: >-
        The boot-owned default root returns one ready StaffService after member attribution,
        with its mailer, renderer and four domain-event subscriptions. Gifts, Stripe and
        Recommendations read the service through call-time getters. The event bus cannot
        remove a handler, so a subscription failure makes the root refuse later init calls
        instead of registering the earlier handlers twice; an owned subscription cleanup
        boundary remains a kernel question.
      reviewedAt: a7dfbfff558768efd13bd1cba076039bdd889733
      sources:
        - core/server/services/staff/index.ts
        - core/server/services/staff/staff-service.js
        - core/server/services/gifts/index.ts
        - core/server/services/stripe/stripe-service.js
        - core/server/services/recommendations/index.ts
        - core/boot.js
        - test/unit/server/services/staff/root.test.ts
      dependencies:
        complexity: straightforward
        reason: >-
          Models, logging, mail, settings, URLs, DomainEvents, attribution, the four event
          constructors and a template reader enter through the root. The reader confines HTML and
          partial reads to the fixed staff files, the root supplies the file access, and Moment
          comes from the kernel clock. The renderer imports Handlebars, the shared delivery
          address formatter and the shared inline-css helper through exact allowances. Gifts,
          Stripe and Recommendations read the Staff service through call-time getters, so
          preserving that access avoids a new construction cycle.
        preparation: []
        reviewedAt: a7dfbfff558768efd13bd1cba076039bdd889733
        sources:
          - core/server/services/staff/index.ts
          - core/server/services/staff/staff-service.js
          - core/server/services/staff/staff-service-emails.js
          - core/server/services/staff/email-template-files.ts
          - core/server/lib/email-rendering/inline-css.ts
          - core/server/lib/mail/format-address-for-delivery.ts
          - core/server/services/gifts/index.ts
          - core/server/services/stripe/stripe-service.js
          - core/boot.js
    entryPoints:
      - staff/index.ts
    notes: >-
      Preserve event-driven staff notifications and direct email/rendering consumers. The prior
      auditedAt records the pre-migration baseline. The bounded migration was inspected on main
      a7dfbfff558768efd13bd1cba076039bdd889733; the root, notification, boot and consumer
      characterization passed on the CommonJS wrapper before production changes. The default
      initializer replaces the wrapper. Ownership of subscriptions, mail transport and pending
      deliveries is unchanged and still undecided.
    contracts:
      exports:
        summary: >-
          The TypeScript root default-exports a defineService initializer with init and a guarded
          service getter that throws IncorrectUsageError before init; there are no extras. The ready
          StaffService exposes the emails helper that Stripe, Gifts and Recommendations use. No
          reset or shutdown method is exposed. Root units pin the export shape, the guard and the
          promise returned by init.
        sources:
          - core/server/services/staff/index.ts
          - core/server/services/staff/staff-service.js
          - core/server/services/stripe/stripe-service.js
          - core/server/services/gifts/index.ts
          - core/server/services/recommendations/service/incoming-recommendation-email-renderer.ts
        tests:
          - test/unit/server/services/staff/root.test.ts
      construction:
        summary: >-
          The root is the only production StaffService constructor found. The factory creates
          GhostMailer and StaffService, which creates StaffServiceEmails, a private Handlebars
          environment, partials from the fixed template reader and helpers. It captures
          memberAttribution.service, injects the event constructors and shared model/settings/URL
          helpers, then subscribes and returns the instance. Direct implementation construction
          outside this root is confined to tests. Root units pin the captured attribution,
          DomainEvents and GhostMailer identities, the emails helper type, and that an unavailable
          attribution root or a renderer failure rejects init with the guard still closed and no
          subscription, with a later init succeeding.
        sources:
          - core/server/services/staff/index.ts
          - core/server/services/staff/staff-service.js
          - core/server/services/staff/staff-service-emails.js
          - core/server/lib/mail/ghost-mailer.js
        tests:
          - test/unit/server/services/staff/root.test.ts
      consumers:
        summary: >-
          Boot initializes the root. Member and subscription events from the members repository
          and milestone events from the milestone repository drive its four subscribers. Stripe
          donation checkout and Gifts resolve service.emails through getters when sending;
          Recommendations passes its renderer a getter over the ready service and owns a separate
          mailer. E2E tests assert signup, subscription, donation, gift and recommendation emails
          through these consumers up to a mocked mail transport. Root units pin that the Stripe
          checkout and Gifts getters throw the readiness error before init and return the current
          service.emails on each access, and drive a members milestone from MilestonesService
          through an in-memory repository into the staff handler and mailer. Recommendations units
          pin the renderer reading the service at send time. No provider delivery is proved.
        sources:
          - core/boot.js
          - core/server/services/staff/staff-service.js
          - core/server/services/members/members-api/repositories/member-repository.js
          - core/server/services/milestones/bookshelf-milestone-repository.js
          - core/server/services/stripe/stripe-service.js
          - core/server/services/stripe/services/webhook/checkout-session-event-service.js
          - core/server/services/gifts/index.ts
          - core/server/services/gifts/gift-service.ts
          - core/server/services/recommendations/index.ts
          - core/server/services/recommendations/service/incoming-recommendation-email-renderer.ts
        tests:
          - test/unit/server/services/staff/index.test.js
          - test/unit/server/services/staff/root.test.ts
          - test/unit/server/services/recommendations/index.test.ts
          - test/e2e-api/members/send-magic-link.test.js
          - test/e2e-api/members/webhooks.test.js
          - test/e2e-api/members/donation-checkout-session.test.js
          - test/e2e-api/members/gift-subscriptions.test.js
          - test/e2e-server/services/recommendation-emails.test.js
      startup:
        summary: >-
          Foreground boot awaits staff init in the services Promise.all group, before mounting the
          app and notifying readiness. Email-address initialization runs earlier; boot awaits
          member-attribution init before entering that group, and the factory captures its ready
          service. Stripe and Gifts are composed earlier but defer reading service.emails. Staff
          init registers listeners and prepares rendering and transport objects; it does not send
          or verify delivery. Boot units pin that staff init waits for member-attribution init and
          that a staff init rejection stops job registration; the booted member-attribution E2E
          pins the shared attribution instance.
        sources:
          - core/boot.js
          - core/server/services/staff/index.ts
          - core/server/services/member-attribution/index.ts
          - core/server/services/stripe/stripe-service.js
          - core/server/services/gifts/index.ts
        tests:
          - test/unit/boot/init-services.test.ts
          - test/unit/server/services/staff/root.test.ts
          - test/e2e-server/services/member-attribution.test.js
      lifetime:
        summary: >-
          The root, service, emails, transport and captured attribution instance survive repeat
          init and server stop. Calling service.subscribeEvents directly registers four more
          handlers without a guard. Events may run concurrently; the service keeps no delivery
          queue or deduplication state. Direct email methods await sequential recipient sends,
          while event dispatch does not await notification completion. Root units pin one set of
          four subscriptions in order across repeated init, a subscription failure leaving earlier
          handlers registered while later init rejects without subscribing again, and the captured
          attribution surviving a root change.
        sources:
          - core/server/services/staff/index.ts
          - core/server/services/staff/staff-service.js
          - core/server/services/staff/staff-service-emails.js
          - core/server/services/members/members-api/repositories/member-repository.js
          - core/server/services/milestones/bookshelf-milestone-repository.js
          - core/server/ghost-server.ts
        tests:
          - test/unit/server/services/staff/root.test.ts
      configuration:
        summary: >-
          GhostMailer captures transport/options on construction, while notifications read
          current eligible users, site settings, sender helper and URL helper when composing
          each message. Mail sending also reads the current email-address service and tracking settings.
          The attribution object and renderer remain captured. Partials load at construction;
          HTML files are read per render and text modules load through require. Notification
          units pin, on one instance, the transport kept after mail config changes, sender,
          title and accent colour read per notification, the sender read per recipient while an
          earlier delivery is pending, and recipient preferences read once per notification.
        sources:
          - core/server/services/staff/index.ts
          - core/server/services/staff/staff-service-emails.js
          - core/server/lib/mail/ghost-mailer.js
          - core/server/models/user.js
          - core/server/services/settings-helpers/settings-helpers.js
        tests:
          - test/unit/server/services/staff/staff-service.test.js
          - test/unit/server/services/staff/notifications.test.ts
          - test/unit/server/lib/mail/ghost-mailer.test.js
      resources:
        summary: >-
          Staff owns its mailer/transport, renderer and four DomainEvents subscriptions; models,
          settings and attribution are borrowed. Neither root nor implementation stores
          unsubscribe handles or pending delivery promises, and boot registers no staff cleanup.
          GhostMailer exposes no close/drain method. Provider-specific connections, queued sends
          and disposal are therefore outside the implemented staff lifecycle. Unit tests assert
          four subscriptions but do not establish cleanup or transport shutdown.
        sources:
          - core/server/services/staff/index.ts
          - core/server/services/staff/staff-service.js
          - core/server/services/staff/staff-service-emails.js
          - core/server/lib/mail/ghost-mailer.js
          - core/boot.js
          - core/server/ghost-server.ts
        tests:
          - test/unit/server/services/staff/staff-service.test.js
          - test/unit/server/services/staff/root.test.ts
      failures:
        summary: >-
          Construction failures reject init with the guard closed and nothing subscribed. A
          subscription failure rejects init and leaves earlier handlers registered; the root then
          refuses later init with IncorrectUsageError. Event handlers catch and log notification
          failures without retry. Historical attribution lookup failure warns and continues, but
          supplied-attribution resolution failure aborts that notification. Recipient lookup,
          rendering or send failure rejects direct email calls and stops later recipients,
          including the loops that await sends before Promise.allSettled. Gifts catch staff-mail
          failures; donation handling propagates them after saving the donation. Invalid milestone
          content warns and skips. Notification units pin each handler's log message, supplied
          versus historical attribution failures, missing member and subscription rows logging a
          TypeError, template failures, and the first rejected send stopping the member, donation
          and milestone loops before any allSettled warning. Checkout units pin donation
          notification failure propagating after the save.
        sources:
          - core/server/services/staff/index.ts
          - core/server/services/staff/staff-service.js
          - core/server/services/staff/staff-service-emails.js
          - core/server/lib/mail/ghost-mailer.js
          - core/server/services/gifts/gift-service.ts
          - core/server/services/stripe/services/webhook/checkout-session-event-service.js
        tests:
          - test/unit/server/services/staff/staff-service.test.js
          - test/unit/server/services/staff/notifications.test.ts
          - test/unit/server/services/staff/root.test.ts
          - test/unit/server/services/stripe/services/webhooks/checkout-session-event-service.test.js
          - test/unit/server/lib/mail/ghost-mailer.test.js
          - test/unit/server/services/gifts/gift-service.test.ts
    unverified: []
    blockers:
      - >-
        Decide an owned cleanup boundary for domain-event subscriptions. The event bus has no
        unsubscribe, so the root can only refuse a retry after a partial subscription; it
        cannot release the handlers that attempt registered.
      - >-
        Decide restart, subscription disposal, provider transport cleanup and pending-delivery
        drain ownership. Server stop currently does not reset staff or wait for its detached
        event work.
      - >-
        Decide whether the apparent Promise.allSettled loops should stop at the first rejected
        awaited send; notification units pin that they do and that no allSettled warning runs.
      - >-
        Milestone persistence-to-mail runs through MilestonesService and an in-memory
        repository in units; no Bookshelf or E2E test asserts delivery, and email_sent_at is
        set before delivery completes. Staff reads milestone.meta.reason while the entity
        carries the reason beside it; units pin that delivery is gated on emailSentAt alone.

  stats:
    kind: directory
    disposition: migrate
    status: audited
    migrated: true
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: straightforward
      modelImpact: fits
      reason: >-
        The boot-owned defineService root applies the Tinybird gates, builds the shared query
        client, assembles the API over the shared connection, URL service, URL helpers and
        logging, and publishes it with the optional cache:stats adapter as one ready aggregate.
        initServices awaits it after Newsletters and before the group containing Explore, which
        receives the ready aggregate. The Admin endpoint can load before init: one shared cache
        forwarder resolves the published cache per request, missing when none is configured so
        the pipeline runs the query once, and each query reads the API per request. No extras or different
        instance lifetime are needed.
      reviewedAt: 8cba94a138a69cbb7e496424591575daab11a529
      sources:
        - core/server/services/stats/index.ts
        - core/server/services/stats/stats-service.js
        - core/server/api/endpoints/stats.js
        - core/server/services/explore-ping/index.ts
        - core/boot.js
        - core/server/services/tinybird/tinybird-service-wrapper.js
      dependencies:
        complexity: straightforward
        reason: >-
          The root now wires every application dependency: it initializes Tinybird under the
          existing gates, builds the query client with the published instance, and passes knex,
          the URL service, URL helpers, logging and the client into StatsService.create, which only
          assembles the query services. Implementations import nothing but errors and the kernel
          clock. The query client still keeps the Tinybird instance from init while rereading
          endpoint and site settings; the root unit pins that mixed lifetime.
        preparation:
          - >-
            Choose a shared owner for the Tinybird request-client factory used by Stats and
            Automations, retaining their different availability gates and transport behavior;
            Automations still builds its own client from the deep factory per operation.
        reviewedAt: 8cba94a138a69cbb7e496424591575daab11a529
        sources:
          - core/server/services/stats/index.ts
          - core/server/services/stats/stats-service.js
          - core/server/services/stats/utils/tinybird.ts
          - core/server/services/stats/posts-stats-service.js
          - core/server/services/automations/automations-api.ts
          - core/server/services/tinybird/tinybird-service-wrapper.js
          - core/server/api/endpoints/tinybird.js
    entryPoints:
      - stats/index.ts
      - stats/utils/tinybird.ts
    notes: >-
      Boot awaits one aggregate of the statistics API and its optional cache before Explore.
      Preserve retained API/cache/query-client relationships and method-specific empty/error
      results.
    contracts:
      exports:
        summary: >-
          The defineService root exports init and a guarded service aggregate with mutable
          api/cache. The deep StatsService constructor/create factory exposes six mutable
          collaborators and async forwarding methods; Explore reaches service.api.mrr directly.
          The separate deep Tinybird module exports create returning receiver-independent
          buildRequest/parseResponse/fetch closures. Factory units assert class identity/client
          presence; selected HTTP responses pin data/meta serialization. The root unit pins the
          guarded service before and during init, the undefined init result and the Admin
          endpoint calling the published api as receiver.
        sources:
          - core/server/services/stats/index.ts
          - core/server/services/stats/stats-service.js
          - core/server/services/stats/utils/tinybird.ts
          - core/server/services/explore-ping/explore-ping-service.ts
        tests:
          - test/unit/server/services/stats/stats.test.ts
          - test/unit/server/services/stats/index.test.ts
          - test/unit/server/services/stats/utils/tinybird.test.js
          - test/e2e-api/admin/stats.test.js
      construction:
        summary: >-
          Import declares only the root. init builds six collaborators from shared knex/URL/URL
          helpers/logging, obtains cache:stats when enabled and publishes both as one aggregate
          once the factory returns; later calls retain it. The root conditionally initializes
          Tinybird and creates one query client for web analytics, without running a statistics
          query, and create only assembles the six query services from the injected set. The
          root unit builds the real root over the shared connection and URL service and pins the
          guarded service during init, cache:stats acquisition, Tinybird gating through the root,
          one query client shared by posts and content, and connection or cache failures leaving
          nothing published before a successful retry rebuilds the aggregate.
        sources:
          - core/server/services/stats/index.ts
          - core/server/services/stats/stats-service.js
          - core/server/services/stats/mrr-stats-service.js
          - core/server/services/stats/members-stats-service.js
          - core/server/services/stats/subscription-stats-service.js
          - core/server/services/stats/referrers-stats-service.js
          - core/server/services/stats/posts-stats-service.js
          - core/server/services/stats/content-stats-service.js
          - core/kernel/adapter-manager/adapter-manager.ts
        tests:
          - test/unit/server/services/stats/stats.test.ts
          - test/unit/server/services/stats/index.test.ts
      consumers:
        summary: >-
          Boot awaits the root before Explore constructs with its ready aggregate; the Admin
          stats endpoint loads without touching the root, and each request resolves
          service.cache through a shared forwarder and reads service.api. Explore retains the aggregate and reads api.mrr under
          its growth/live-Stripe gates. Automations destructures the deep query factory and
          supplies its current Tinybird instance per operation. Real Admin HTTP snapshots pin
          counts/MRR/subscriptions/referrers and selected empty-input shapes; trial/incomplete
          subscription tests compare before/after results. Several top-post/referrer/growth
          tests assert only array shape, and invalid-ID cases do not assert empty counts.
          Explore units inject the stats graph; automation composition is separately recorded
          under Tinybird. The root unit pins the Admin endpoint loading before init, rejecting a
          query until init, and forwarding to the published cache, the API per request and the
          query directly once the cache is null; the app-allowlist unit loads Admin routes with
          no boot. Configured
          stats-cache hits/identity/keys are not covered here.
        sources:
          - core/boot.js
          - core/server/api/endpoints/stats.js
          - core/server/api/endpoints/index.js
          - core/server/web/api/endpoints/admin/routes.js
          - core/server/services/explore-ping/index.ts
          - core/server/services/explore-ping/explore-ping-service.ts
          - core/server/services/automations/automations-api.ts
        tests:
          - test/e2e-api/admin/stats.test.js
          - test/unit/server/services/stats/index.test.ts
          - test/unit/server/services/explore-ping/explore-ping-service.test.ts
          - test/e2e-api/admin/automations.test.ts
      startup:
        summary: >-
          After model/settings setup, awaited Stripe startup and Newsletters, boot awaits
          stats.init before the service Promise.all that holds Explore, then jobs
          registration/start and HTTP readiness. Captured knex and settings must be usable before
          queries; reading the service before init throws, but the Admin endpoint reads it only
          per request. Factory errors, including cache
          acquisition, reject boot without publishing. Existing HTTP suites exercise successful
          real boot and queries; the root unit covers early endpoint import, repeated init and
          failure of either construction stage with injected failures, not real deferred
          readiness or reboot.
        sources:
          - core/boot.js
          - core/server/services/stats/index.ts
          - core/server/services/stats/stats-service.js
          - core/server/api/endpoints/stats.js
          - core/server/web/api/endpoints/admin/routes.js
          - core/shared/express.js
        tests:
          - test/e2e-api/admin/stats.test.js
          - test/unit/server/services/stats/index.test.ts
          - test/e2e-server/services/stats/mrr-stats-service.test.js
      lifetime:
        summary: >-
          The first published aggregate persists through subsequent init calls; no reset,
          shutdown or rebind exists. Components retain database/URL/query clients. The query
          client retains its token service while rereading config/settings facades; existing
          consumer units assert old token identity with new endpoint/site values and a new
          client's replacement token. The pipeline retains the endpoint's forwarder, which
          resolves the aggregate's cache per request;
          stats installs no invalidation listener. Adapter-manager clearCache does not update
          retained references. The root unit pins the retained api across repeated init with no
          later cache acquisition, and the query client keeping its Tinybird instance while
          rereading endpoint and site settings. Pending query, cache refresh and stop/reboot
          behavior is source-only.
        sources:
          - core/server/services/stats/index.ts
          - core/server/services/stats/stats-service.js
          - core/server/services/stats/utils/tinybird.ts
          - core/server/api/endpoints/stats.js
          - core/kernel/adapter-manager/adapter-manager.ts
          - package.json
        tests:
          - test/unit/server/services/tinybird/consumer-configuration.test.ts
          - test/unit/server/services/stats/index.test.ts
      configuration:
        summary: >-
          Initialization captures cache enablement and web-analytics client presence; the root
          also initializes Tinybird whenever stats config is truthy, without a labs gate. Default
          windows and today come from the kernel clock through native UTC day helpers; DB values,
          caller dates and timezone-aware boundaries still go through the clock's Moment export. Request-time
          date/filter options and config/settings lookups differ from those captured choices;
          test config replacement is not production hot reload. Member forwarding maps dateFrom
          to startDate; missing newsletter IDs return method-specific empty values. Real SQLite
          class tests pin selected MRR history/currency, member/gift deltas, custom-start output,
          subscription tier/cadence and referrer totals. Posts tests pin selected attribution,
          view fallback/backfill, page exclusion and UTC subscriber values. Units pin the 91-day
          member and 90-day MRR/referrer default windows at their UTC day boundaries, UTC day
          truncation of custom start dates, timezone day boundaries across a DST transition,
          inclusive date filters and timezone keying of event instants. They use simplified
          schema and injected provider/URL collaborators, not complete root/MySQL composition.
        sources:
          - core/server/services/stats/stats-service.js
          - core/server/services/stats/index.ts
          - core/server/services/stats/utils/tinybird.ts
          - core/server/services/stats/utils/date-utils.js
          - core/server/services/stats/mrr-stats-service.js
          - core/server/services/stats/members-stats-service.js
          - core/server/services/stats/subscription-stats-service.js
          - core/server/services/stats/referrers-stats-service.js
          - core/server/services/stats/posts-stats-service.js
          - core/server/services/stats/content-stats-service.js
          - core/shared/config/loader.ts
        tests:
          - test/unit/server/services/stats/stats.test.ts
          - test/unit/server/services/stats/index.test.ts
          - test/unit/server/services/stats/mrr.test.js
          - test/unit/server/services/stats/members.test.js
          - test/unit/server/services/stats/subscriptions.test.js
          - test/unit/server/services/stats/referrers.test.js
          - test/unit/server/services/stats/posts.test.js
          - test/unit/server/services/stats/utils/date-utils.test.ts
          - test/e2e-server/services/stats/mrr-stats-service.test.js
      resources:
        summary: >-
          Components borrow knex/URL/query clients and the shared logger and own no pool, timer
          or subscription. Independent
          queries do not share a snapshot; enrichment fans out lookups, and date ranges can
          allocate uncapped daily series. Shared request-external owns agents; query options ask
          for 35-second timeout/zero retries, with POST AbortSignal. Optional cache adapters can
          own timers, background refresh and Redis clients; stats registers no cleanup or drain.
          Units assert query-client options with fake HTTP; SQLite/HTTP statistics assertions
          do not prove query cancellation, transport/cache release or consistent concurrent reads.
        sources:
          - core/server/services/stats/index.ts
          - core/server/services/stats/utils/tinybird.ts
          - core/server/services/stats/posts-stats-service.js
          - core/server/services/stats/content-stats-service.js
          - core/server/services/stats/members-stats-service.js
          - core/server/lib/request-external.js
          - core/server/adapters/cache/AdapterCacheMemoryTTL.js
          - core/server/adapters/lib/redis/AdapterCacheRedis.js
          - core/server/adapters/lib/redis/redis-store-factory.js
          - core/boot.js
        tests:
          - test/unit/server/services/stats/utils/tinybird.test.js
      failures:
        summary: >-
          A failed cache acquisition rejects init without publishing, and a later init rebuilds
          the whole aggregate, including Tinybird initialization and a new query client. Uncaught
          MRR/member/subscription/referrer query failures reject; posts/content methods commonly
          log and substitute different empty arrays, objects or zero-series shapes. Content
          has false url_exists when missing/not-ready, unlike posts' true default; thrown URL
          lookup changes content's existence fallback to true. Tinybird build errors precede
          its fetch catch; caught transport/parse errors return null. Posts view queries backfill
          on absent/empty provider data but return empty data on rejected fetch. Selected units
          pin fake provider/URL failure and fallback results. The first two content client tests
          invoke only stubs; they prove no production transport behavior. The root unit pins both
          construction failures and their retries with injected failures; real cache errors and
          many nested query failure channels lack assertions.
        sources:
          - core/server/services/stats/index.ts
          - core/server/services/stats/utils/tinybird.ts
          - core/server/services/stats/mrr-stats-service.js
          - core/server/services/stats/members-stats-service.js
          - core/server/services/stats/subscription-stats-service.js
          - core/server/services/stats/referrers-stats-service.js
          - core/server/services/stats/posts-stats-service.js
          - core/server/services/stats/content-stats-service.js
        tests:
          - test/unit/server/services/stats/utils/tinybird.test.js
          - test/unit/server/services/stats/index.test.ts
          - test/unit/server/services/stats/posts.test.js
          - test/unit/server/services/stats/content.test.js
    unverified: []
    blockers:
      - >-
        Stop/reboot is not pinned: the aggregate persists across the e2e lane's reboots with the
        settings and Tinybird instance captured at first init, and a retry after a failed init
        rebuilds Tinybird's shared instance and a new query client.
      - >-
        Add real configured-cache consumer tests for early/later endpoint construction, shared
        identity, hits, invalidation/expiry, authorization and option isolation. The API pipeline
        returns cache hits before its permission stage; no role-isolation outcome is inferred.
        postReferrers
        allows date/order/limit/timezone options but its key contains only method and post ID;
        no existing stats HTTP assertion establishes correct distinct cached responses.
      - >-
        The captured web-analytics gate and token identity versus reread endpoint and site
        settings are pinned through the root; protect actual URL readiness/error behavior.
        Preserve method-specific empty/error/fallback
        distinctions, including rejected versus empty provider results, through real consumers
        rather than only injected collaborators or stub-self-asserting client tests.
      - >-
        Strengthen specific weak business assertions before moving composition: three member-
        attribution cases permit empty results; the page-growth fixture creates post-type events;
        referrer ranking tests re-sort actual output. Subscription-upgrade totals and UTC
        timezone/day boundaries are now compared. Pin true page attribution, filter/value forms,
        MySQL query failures and recovery. Existing real SQLite
        totals are useful evidence, not proof of every named contract or concurrent snapshots.
      - >-
        Establish ownership and limits for optional cache/HTTP resources, in-flight queries,
        unbounded date ranges and lookup fan-out. A stopped shared database or replaced token
        root does not reset this graph; no cancellation or cache/client disposal is exposed.
  stripe:
    kind: directory
    disposition: migrate
    status: audited
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: complex
      modelImpact: fits
      reason: >-
        The retained controller graph already owns configure, connect, disconnect and
        shutdown operations. Preserve per-boot configuration, tolerated configuration
        failures, ephemeral-webhook cleanup and deferred Members/Gifts/Staff access outside
        one-time construction. This requires careful ordering and failure-policy work, but
        reconfiguration of a stable owner does not itself require a kernel change.
      reviewedAt: f276513ba32b7114fd5cc4f7b02d3f82c0c31e3e
      sources:
        - core/server/services/stripe/service.js
        - core/server/services/stripe/stripe-service.js
        - core/boot.js
      dependencies:
        complexity: complex
        reason: >-
          Stripe builds API, webhook and billing collaborators before Members, Gifts, Staff and
          checkout configuration are ready, deliberately using getters for later operations. Nested
          code still imports those services, Welcome Emails, config/labs/i18n, event dispatch,
          Stripe SDK and rate-limit construction. Configuration replaces provider clients later, so
          eagerly moving every require to a one-time factory would change both availability and
          configuration timing.
        preparation:
          - >-
            Pass explicit deferred member/gift/staff/checkout/metafield capabilities from index.ts
            and remove nested service imports without turning operation-time getters into startup
            reads.
          - >-
            Inject SDK and rate-limit factories plus logging/config/translation/event dependencies,
            retaining configuration-time acquisition and live tax-policy behavior.
          - >-
            Separate one-time graph construction from configure/reconfigure.
        reviewedAt: 44ad405bff4a8700af2ed51f833ebc08d25dddff
        sources:
          - core/server/services/stripe/service.js
          - core/server/services/stripe/stripe-service.js
          - core/server/services/stripe/stripe-api.js
          - core/server/services/stripe/config.js
          - core/server/services/stripe/services/webhook/checkout-session-event-service.js
          - core/server/services/members/api.js
          - core/boot.js
    entryPoints:
      - stripe/index.js
    notes: >-
      Preserve the eager composite, stable API/controller references and deferred dependency
      getters. Configuration changes nested clients and remote registration; shutdown only
      removes an ephemeral webhook. These are separate lifetimes, not one disposable instance.
    contracts:
      exports:
        summary: >-
          CommonJS index forwards one StripeService instance with an added async init method. It
          exposes api, webhookController, webhookManager, billingPortalManager, migrations,
          models and async configure/connect/disconnect/shutdown methods. Configure, disconnect
          and shutdown require their receiver; connect does not, and added init closes over the
          module export. API configuration is synchronous; before configuration configured is
          false, mode reports live and testEnv dereferences absent config. Most operations have
          no shared readiness guard. Deep class units assert selected API arguments/results and
          controller status codes, not the real root shape, method binding, pre-init behavior or
          retained identity.
        sources:
          - core/server/services/stripe/index.js
          - core/server/services/stripe/service.js
          - core/server/services/stripe/stripe-service.js
          - core/server/services/stripe/stripe-api.js
          - core/server/services/stripe/webhook-controller.js
        tests:
          - test/unit/server/services/stripe/stripe-api.test.js
          - test/unit/server/services/stripe/webhook-controller.test.js
      construction:
        summary: >-
          Import eagerly constructs the service, API, migrations, webhook and billing managers,
          four event services and controller. Picked models and the API object are retained.
          Getter dependencies defer members repositories, donation repository, gifts service,
          staff emails until handler work; the welcome-email check reads the ready root per call.
          The API holds no checkout config: whatever opens each Checkout session passes the
          checkout the checkout-config's checkout service decided. The SDK
          client and two rate buckets are acquired by each successful non-null API configure. No
          guard coalesces repeated or overlapping root init/configure, and no rollback covers
          partially configured collaborators. No focused real-root
          construction/dependency-capture or WebhookManager lifecycle test was found; injected
          handler tests do not protect these getters against eager capture.
        sources:
          - core/server/services/stripe/service.js
          - core/server/services/stripe/stripe-service.js
          - core/server/services/stripe/stripe-api.js
          - core/server/services/stripe/webhook-manager.js
          - core/server/services/stripe/services/webhook/checkout-session-event-service.js
          - core/server/services/stripe/services/webhook/invoice-event-service.js
          - core/server/services/stripe/checkout-session-params.ts
        tests:
          - >-
            test/unit/server/services/stripe/services/webhooks/checkout-session-event-service.test.js
          - test/unit/server/services/stripe/services/webhooks/invoice-event-service.test.js
      consumers:
        summary: >-
          The members app retains a bound webhook-controller method when constructed before
          initServices. Members API/import dependencies retain stripe.api; members startup
          invokes migrations. Gifts resolves the API through a getter. Checkout previews read
          the current Stripe API configured flag through a callback, then create a session
          through members payments. Settings BREAD awaits connect before persisting changed
          settings; the settings endpoint awaits disconnect before clearing keys. These bindings
          are source-checked. Signed webhook HTTP tests exercise the real
          parser/controller/model graph and assert invalid-signature refusal, unknown-event
          success and selected subscription/member persistence with provider calls intercepted.
          Donation/gift checkout HTTP suites then use StripeMocker.sendWebhook, which directly
          invokes the real controller's handleEvent, bypassing HTTP signature parsing. They
          prove selected persistence/mail outcomes, not all retained bindings, replacement
          behavior or remote provider delivery.
        sources:
          - core/server/web/members/app.js
          - core/server/services/members/api.js
          - core/server/services/members/service.js
          - core/server/services/members/members-api/members-api.js
          - core/server/services/gifts/index.ts
          - core/server/services/settings/settings-bread-service.js
          - core/server/api/endpoints/settings.js
          - core/server/services/stripe/stripe-service.js
          - core/server/services/stripe-checkout-config/index.ts
          - core/server/services/stripe-checkout-config/preview-service.ts
        tests:
          - test/e2e-api/members/webhooks.test.js
          - test/e2e-api/members/donation-checkout-session.test.js
          - test/e2e-api/members/gift-subscriptions.test.js
          - test/utils/stripe-mocker.js
      startup:
        summary: >-
          Boot awaits init before gifts and the remaining Promise.all group, including members,
          donations and staff. A held-promise boot harness asserts gifts, mentions, analytics,
          media and job registration/start remain untouched until Stripe resolves, using fake
          collaborators; members/donations/staff ordering is source-established. Real init logs
          configuration failures and still registers the settings listener, so this barrier is
          not a guarantee of successful Stripe readiness. Configure replaces the API client
          first, configures the controller and managers, awaits webhook start, then awaits
          billing start outside test environments. Boot registers Stripe cleanup only after
          gifts initializes successfully. Normal booted webhook tests establish working
          composition, not failure at every acquisition stage or real remote registration.
        sources:
          - core/boot.js
          - core/server/services/stripe/service.js
          - core/server/services/stripe/stripe-service.js
          - core/server/services/stripe/webhook-manager.js
          - core/server/services/stripe/billing-portal-manager.js
        tests:
          - test/unit/boot/init-services.test.ts
          - test/e2e-api/members/webhooks.test.js
      lifetime:
        summary: >-
          Root, API and controller identities survive init/configure; successful API configure
          replaces nested SDK/buckets. Init removes and re-adds the same settings listener;
          relevant edits share a 600ms debounce whose asynchronous configuration can overlap
          earlier work. No keys makes configureApi return without disabling an existing client;
          disconnect explicitly disables it but retains prior API config/mode/buckets. Supplying
          a webhook secret switches the manager to local mode; a later absent secret does not
          restore network mode or clear it. Stop does not clear the in-memory secret. Pending
          throttled operations use whichever SDK exists after waiting. These are source-only
          contracts, without root reconfiguration, restart or concurrent-work assertions. Gift
          HTTP's sequential duplicate dispatch asserts one row for the existing gift ID, not
          concurrent webhook exclusion or once-only notification effects.
        sources:
          - core/server/services/stripe/service.js
          - core/server/services/stripe/stripe-service.js
          - core/server/services/stripe/stripe-api.js
          - core/server/services/stripe/webhook-manager.js
        tests:
          - test/e2e-api/members/gift-subscriptions.test.js
      configuration:
        summary: >-
          Configuration reads active settings keys and the immutable config facade when invoked,
          then captures key, URL, promo and environment values; shared URL helpers freeze their
          base URLs. Automatic-tax remains a labs getter and additional-payment-method checks
          happen per call. Whatever opens each checkout passes the saved design from the
          checkout-config's checkout service, and a preview passes the design being previewed.
          Checkout HTTP tests assert saved/cleared designs and malformed stored-design fallback
          with Stripe mocked, not real root replacement or
          provider acceptance. Nonproduction normally supplies a local placeholder secret;
          explicit remote-webhook mode omits it and marks registration ephemeral. Production
          registration remains persistent. Config units inject key/URL helpers and test-only
          config/environment changes, asserting exact webhook URL, secret/mode flags and
          normalized ignore lists; they do not establish production hot reload. API units
          replace the SDK and assert selected checkout/customer/metadata/tax fields and gift
          idempotency-key forwarding; translated gift titles use a stubbed translator. They do
          not prove real SDK client rotation, rate limiting or preservation of current/captured
          configuration boundaries.
        sources:
          - core/server/services/stripe/config.js
          - core/server/services/stripe/service.js
          - core/server/services/stripe/stripe-service.js
          - core/server/services/stripe/stripe-api.js
          - core/server/services/settings-helpers/settings-helpers.js
          - core/shared/config/loader.ts
          - core/shared/url-utils.ts
          - core/server/services/stripe-checkout-config/index.ts
          - core/server/services/stripe-checkout-config/checkout-service.ts
        tests:
          - test/unit/server/services/stripe/config.test.js
          - test/unit/server/services/stripe/stripe-api.test.js
          - test/e2e-api/members/create-stripe-checkout-session.test.js
      resources:
        summary: >-
          The composite borrows model connections, settings and domain events; it owns nested
          clients, rate queues, debounced configuration and remote webhook/billing registration
          work. Installed Stripe uses shared keep-alive agents, an 80-second socket-inactivity
          timeout and zero default network retries; the root supplies no transport timeout/retry
          override or drain. LeakyBucket retains callbacks and timers while queued; replacement
          does not cancel old buckets. Shutdown only awaits stop for ephemeral registrations: it
          leaves the settings listener/debounce, clients, pending HTTP/handlers and remote
          billing state. Production webhook registration is retained. Manager stop swallows
          deletion/persistence failures as false, which shutdown/disconnect ignore. Domain
          dispatch is unawaited and test allSettled is not a production drain. Paid signup mail
          is launched without awaiting its promise inside the checkout handler, so it can
          outlive the webhook response and reject outside its catch; handler units assert
          invocation only. Billing units assert fake API/settings registration paths, not actual
          sockets, timer cancellation or remote cleanup.
        sources:
          - core/server/services/stripe/service.js
          - core/server/services/stripe/stripe-service.js
          - core/server/services/stripe/stripe-api.js
          - core/server/services/stripe/webhook-manager.js
          - core/server/services/stripe/billing-portal-manager.js
          - core/server/services/stripe/services/webhook/checkout-session-event-service.js
          - core/server/ghost-server.ts
          - package.json
        tests:
          - test/unit/server/services/stripe/billing-portal-manager.test.js
          - >-
            test/unit/server/services/stripe/services/webhooks/checkout-session-event-service.test.js
      failures:
        summary: >-
          Callers pass each session's checkout; the checkout-config's checkout service logs a
          config read failure and gives none. Refusal of a checkout's design retries once without
          it; refusal of a preview's design becomes ValidationError
          and other provider errors propagate. SDK-fake units assert these branches, but do not
          check fallback preservation of request options or idempotency keys. Root init logs
          configuration errors; direct configure rejects after any earlier client, manager or
          remote effects. Webhook creation precedes saving its ID/secret, and failed persistence
          can leave remote state without publication. Billing update/replacement failures can
          retain the old ID; initial creation and settings-save errors propagate. Disconnect
          performs sequential database changes without one encompassing transaction, then
          attempts webhook removal and disables the API. Migrations catch their sequence's
          errors internally, so the members runner can record finished after a logged failure.
          Controller source awaits handlers before responding, logs version mismatch but still
          processes, and returns error status or 500; no root event-ID deduplication exists.
          Controller units inject a parser and synchronous handler failure; real signed HTTP
          covers selected success/refusal paths. Migration units use fake transactions and pin
          skipping/default-name behavior. Booted members-runner tests use real job rows but
          substitute execute, proving skip/retry, caught rejection and selected insert-failure
          behavior; they do not exercise a real Stripe migration sequence failure. Two
          duplicate-subscription units assert truthy err.name rather than equality and catch
          their own assert.fail, so they do not even guarantee the expected rejection or its
          ConflictError type.
          Historical evidence removed after auditedAt: [Stripe migration source](https://github.com/TryGhost/Ghost/blob/bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e/ghost/core/core/server/services/stripe/stripe-migrations.js), [Stripe migration tests](https://github.com/TryGhost/Ghost/blob/bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e/ghost/core/test/unit/server/services/stripe/migrations.test.js), [Members migration tests](https://github.com/TryGhost/Ghost/blob/bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e/ghost/core/test/e2e-server/services/members-migrations.test.ts).
        sources:
          - core/server/services/stripe/service.js
          - core/server/services/stripe/stripe-service.js
          - core/server/services/stripe/webhook-manager.js
          - core/server/services/stripe/billing-portal-manager.js
          - core/server/services/stripe/webhook-controller.js
          - core/server/services/members/service.js
          - core/server/services/stripe/services/webhook/subscription-event-service.js
          - core/server/services/stripe/stripe-api.js
        tests:
          - test/unit/server/services/stripe/webhook-controller.test.js
          - test/unit/server/services/stripe/billing-portal-manager.test.js
          - >-
            test/unit/server/services/stripe/services/webhooks/subscription-event-service.test.js
          - test/e2e-api/members/webhooks.test.js
          - test/unit/server/services/stripe/stripe-api.test.js
    unverified: []
    blockers:
      - >-
        Before changing initialization, pin real import/graph identity, receiver contracts,
        pre-init access, deferred dependency getters, retained members/webhook consumers and
        partial construction. Existing fake boot barriers protect awaited ordering, not real
        root publication or successful dependency readiness. Include the checkout-config
        preview callbacks into members payments; current checkout tests do not hold or replace
        those roots.
      - >-
        Characterize repeated/overlapping init and reconfiguration, no-key and local/network
        transitions, queued old-client work and retry after each partial failure. Decide changed
        semantics separately; preserving current behavior must not silently clear or replace
        objects that consumers retain.
      - >-
        Establish shutdown ownership with held debounce, throttle, provider and handler work;
        test ephemeral cleanup failure, same-process restart and persistent registration reuse.
        Remote registration/settings persistence and disconnect are not atomic, and current
        shutdown provides neither a complete drain nor client/listener disposal.
      - >-
        Add held-handler and actual persistence failure assertions before changing webhook or
        migration orchestration. Repair weak duplicate-error assertions; cover concurrent and
        out-of-order effects in the relevant owning services. Broader payment business behavior
        and unrelated existing gaps do not gate a composition-only eight-service pilot. Pin
        fallback request options and idempotency-key forwarding before changing checkout retry
        composition.
  stripe-checkout-config:
    kind: directory
    disposition: migrate
    status: auditing
    migrated: true
    extras:
      actingContext:
        purpose: Convert API authentication context into the staff actor recorded in the config's history.
        callers:
          - core/server/api/endpoints/stripe-checkout-config.ts
        timing: Pure conversion when an API command runs; it does not require initialization.
        dependencies: []
        failurePolicy: A missing staff user or an integration yields a null actor; permission checks remain with the API.
        destination: A normalized request actor supplied by the API framework Frame, with actor policy owned by the application.
    migration:
      complexity: straightforward
      modelImpact: fits
      reason: >-
        Boot awaits the defineService initializer in initCore after the custom fields services.
        It builds the config, checkout, preview and branding services as one ready aggregate from
        member-metafield bindings, knex, models, labs and logging, and keeps the deferred Stripe,
        tier and member callbacks so construction does not pull later boot dependencies
        forward. The constructors own no startup resources.
      reviewedAt: f276513ba32b7114fd5cc4f7b02d3f82c0c31e3e
      sources:
        - core/server/services/stripe-checkout-config/index.ts
        - core/server/services/stripe-checkout-config/service.ts
        - core/server/services/stripe-checkout-config/preview-service.ts
        - core/boot.js
      dependencies:
        complexity: complex
        reason: >-
          Checkout config starts before Stripe and Members, then uses deferred callbacks back into
          those consumers for preview/branding. The root injects logging into the config row codec
          factory, the history recorder and the branding service, and gives the checkout service
          the config service, labs and logging; the Stripe Checkout constraints package, the
          metafield identity schemas, the request parser and the case-key helpers are allowlisted
          by name, lodash is gone, and the metafield status is a type-only import. API output
          serializers import the wire serializers from the service's own module rather than the
          root.
        preparation:
          - >-
            Keep preview/branding callbacks deferred and characterize the
            config-to-Stripe-to-Members dependency path before changing boot order or instance
            capture.
        reviewedAt: 44ad405bff4a8700af2ed51f833ebc08d25dddff
        sources:
          - core/server/services/stripe-checkout-config/index.ts
          - core/server/services/stripe-checkout-config/codec.ts
          - core/server/services/stripe-checkout-config/models.ts
          - core/server/services/stripe-checkout-config/service.ts
          - core/server/services/stripe-checkout-config/checkout-service.ts
          - core/server/api/endpoints/stripe-checkout-config.ts
    entryPoints:
      - stripe-checkout-config/index.ts
    notes: >-
      Site-wide Stripe Checkout config, and the checkout service that decides each checkout from
      it: members payments, card updates and gifts are handed that service and pass its checkout
      to the Stripe API, and previews of unsaved settings on a real Stripe Checkout page decide
      one from those settings. It also reads the branding set in the Stripe dashboard. The
      checkout webhook reads only the collection flag's name from it.
    contracts:
      exports:
        summary: >-
          The TypeScript root default-exports defineService with init, the guarded service getter
          for the config, checkouts, previews and branding services, and actingContext as its only
          extra. Its named exports are type-only. API endpoints read the services through service;
          the members API reads checkouts when it is built and boot passes it to gifts init; and
          the API's output serializers and the collection-flag checks import the wire serializers
          and the collected parts' constants from the service's own modules.
        sources:
          - core/kernel/define-service.ts
          - core/server/services/stripe-checkout-config/index.ts
        tests:
          - test/unit/server/services/stripe-checkout-config/index.test.ts
      construction:
        summary: >-
          Boot awaits init in initCore after the custom fields services. The initializer builds
          the config service with knex, its history recorder, logging and the custom field
          bindings it saves the shipping address and phone number into, and throws if those
          bindings aren't set up yet. It builds the checkout service over that config service,
          labs and logging, the preview service with that checkout service and lookups of Stripe,
          tiers and the members payments service made per preview, and the branding service with
          lookups of Stripe made per read, as those start later in boot. Later init calls keep
          the same services.
        sources:
          - core/server/services/stripe-checkout-config/index.ts
          - core/boot.js
        tests:
          - test/unit/server/services/stripe-checkout-config/index.test.ts
          - test/unit/boot/init-core.test.ts
    unverified:
      - consumers
      - startup
      - lifetime
      - configuration
      - resources
      - failures
    blockers: []
  tags-public:
    kind: directory
    disposition: migrate
    futureHome: core/server/api/endpoints/utils/public-response-cache
    status: audited
    migrated: true
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: straightforward
      modelImpact: fits
      reason: >-
        The boot-owned default root returns one facade with an optional cache. Disabled
        caching still yields a ready instance. The endpoint captures service.cache after
        initialization, preserving adapter identity and the single site.changed listener.
        Registration remains the final resource acquisition; no new lifecycle hook is needed.
      reviewedAt: 0780d0a76e340fd2d131e782a164633c4d389084
      sources:
        - core/server/services/tags-public/index.ts
        - core/server/api/endpoints/tags-public.js
        - core/boot.js
      dependencies:
        complexity: straightforward
        reason: >-
          The migrated root wires the cache adapter, its enablement config and the site-change
          event bus in index.ts. The Content API captures the ready cache facade; this
          composition is independent of the larger post and URL service migrations.
        preparation: []
        reviewedAt: 0780d0a76e340fd2d131e782a164633c4d389084
        sources:
          - core/server/services/tags-public/index.ts
          - core/server/api/endpoints/tags-public.js
          - core/boot.js
    entryPoints:
      - tags-public/index.ts
    notes: >-
      Suggested future home after this migration: API-owned public-response-cache support alongside the posts cache.
      This root only selects the tags cache and subscribes to site invalidation; tag querying is
      endpoint-owned. Boot still owns configured cache readiness, and generic adapter mechanics
      retain their own owner. No source has moved. The prior auditedAt records the pre-migration
      baseline. The bounded migration was inspected on main 0780d0a76e340fd2d131e782a164633c4d389084;
      its root/binding units, isolated cold-boot HTTP tests including the Admin tag-edit
      invalidation case, and tag query privacy HTTP passed before production changes. The
      default initializer replaces the wrapper without extras, exactly as posts-public. Cache
      configuration, identity and invalidation are unchanged; early controller imports now fail
      through the readiness guard. Feature-local boolean parsing preserves legacy truthiness
      without tightening accepted configuration. Restart, adapter failures and asynchronous
      invalidation remain separate obligations below.
    contracts:
      exports:
        summary: >-
          The TypeScript root default-exports a defineService initializer with init and a
          guarded service getter; there are no extras. The ready service is one facade
          containing cache, including undefined when disabled. It exposes no tag query, reset,
          stop or disposer. Units assert enabled/disabled cache identity and endpoint bindings;
          generic readiness, shared initialization and retry belong to kernel tests.
        sources:
          - core/kernel/define-service.ts
          - core/server/services/tags-public/index.ts
          - core/server/api/endpoints/tags-public.js
        tests:
          - test/unit/server/services/public-response-caches.test.ts
      construction:
        summary: >-
          Import declares the root without resolving runtime collaborators. First init requires
          the shared adapter manager, config facade and common event bus; a truthy
          tagsPublicCache flag obtains cache:tagsPublic and registers its reset listener as the
          final resource acquisition before returning the facade. Boot awaits init before the
          guarded service is ready. Manager identity is requested name plus selected class. No
          other production constructor or deep consumer was found. Units stub config/adapter
          lookup and assert the exact adapter name and identity, with the posts flag opposite to
          catch cross-wiring.
        sources:
          - core/server/services/tags-public/index.ts
          - core/server/services/adapter-manager/index.ts
          - core/kernel/adapter-manager/adapter-manager.ts
        tests:
          - test/unit/server/services/public-response-caches.test.ts
      consumers:
        summary: >-
          Boot initializes the root; tags browse captures service.cache when its endpoint loads.
          The shared pipeline preserves the adapter identity and memoizes the controller wrapper.
          Tags read is uncached. Content routes and frontend get/routing-data calls reach these
          public methods through API getters; query implementation belongs to TagPublic/model
          and API serializers. Binding units assert enabled/disabled browse and uncached read.
          Isolated HTTP proves a repeated browse avoids another model call despite a raw DB
          edit, changed fields misses, and explicit site.changed refreshes. It also warms tag
          browse, changes a tag through the authenticated Admin endpoint, and proves the next
          Content browse queries fresh data and the following read is served from cache, with
          no manual invalidation in that mutation path. Frontend tests use API fakes; this
          cohort does not establish their real cache composition.
        sources:
          - core/boot.js
          - core/server/api/endpoints/index.js
          - core/server/api/endpoints/tags.js
          - core/server/api/endpoints/tags-public.js
          - core/server/models/tag-public.js
          - core/server/web/api/endpoints/content/routes.js
          - core/frontend/services/proxy.js
          - core/frontend/services/routing/config.ts
          - core/frontend/services/routing/api-adapter.ts
          - core/frontend/services/data/fetch-data.js
          - core/frontend/helpers/get.js
          - core/server/web/parent/middleware/emit-events.js
        tests:
          - test/unit/server/services/public-response-caches.test.ts
          - test/e2e-server/public-response-caches.isolated.test.ts
          - test/unit/frontend/services/data/fetch-data.test.js
          - test/unit/frontend/helpers/get.test.ts
      startup:
        summary: >-
          Boot validates adapters and prepares models/config before awaiting tags init in its
          service group, before the full app mounts. Content router/API construction is lazy,
          normally loading this endpoint after initialization. The isolated HTTP suite drives
          that cold boot with MemoryCache enabled. Reading service while importing the
          controller before readiness now throws rather than silently binding no cache. The
          root does not initialize on a request, await external cache readiness or register
          rollback after later boot failure.
        sources:
          - core/boot.js
          - core/server/services/tags-public/index.ts
          - core/server/api/endpoints/index.js
          - core/server/api/endpoints/tags-public.js
          - core/server/web/api/app.js
          - core/server/web/api/endpoints/content/app.js
          - core/shared/express.js
        tests:
          - test/e2e-server/public-response-caches.isolated.test.ts
      lifetime:
        summary: >-
          Successful init retains the same facade, adapter, entries and listener, including a
          facade with no cache when disabled. There is no listener disposer or supported
          service reset. Clearing the manager cache does not replace root, listener or endpoint
          references. MemoryCache stores response references and swaps its backing object on
          reset. Units assert repeated enabled init preserves adapter/data with one new listener
          and directly invoke that callback; HTTP also emits on the real bus. Same-module
          reboot, forced re-init, response-reference mutation and concurrent fill/reset remain
          source-only.
        sources:
          - core/server/services/tags-public/index.ts
          - core/kernel/adapter-manager/adapter-manager.ts
          - core/server/adapters/cache/MemoryCache.js
          - core/server/api/endpoints/tags-public.js
          - core/server/lib/common/events.js
          - core/server/ghost-server.ts
        tests:
          - test/unit/server/services/public-response-caches.test.ts
          - test/e2e-server/public-response-caches.isolated.test.ts
      configuration:
        summary: >-
          The root reads hostSettings:tagsPublicCache:enabled only on first init through a
          feature-local coercing boolean schema. It preserves legacy truthiness; missing/falsy
          disables. Feature-specific adapter options fall back to the shared MemoryCache
          default. Production config is frozen; test overrides do not imply hot reload. Tags
          has no custom cache key: the pipeline serializes configured frame.options, including
          copied request context, with sorted object keys before validation/input normalization.
          HTTP cache coverage checks a fields variant, not every option/context. Separate query
          privacy HTTP asserts hidden filter/order/selectors are ignored and public identifiers,
          visibility, slug order and post counts remain; it does not enable this response cache.
        sources:
          - core/server/services/tags-public/index.ts
          - core/kernel/adapter-manager/options.ts
          - core/shared/config/defaults.json
          - core/shared/config/SCHEMA.md
          - core/server/api/endpoints/tags-public.js
          - core/server/api/endpoints/utils/serializers/input/tags.js
          - core/server/api/endpoints/utils/api-filter-utils.ts
        tests:
          - test/unit/server/services/public-response-caches.test.ts
          - test/e2e-server/public-response-caches.isolated.test.ts
          - test/e2e-api/content/tag-query-security.test.js
      resources:
        summary: >-
          Root owns an unremoved event callback and borrows the manager-cached adapter. Default
          MemoryCache is unbounded with no TTL, timer or I/O. Optional memory-ttl wraps TTLCache
          with unref'd expiry timers cleared on reset. Redis can share a process store/client,
          registers an error listener and tracks lookup timers, coalesced reads and background
          refreshes; timing out does not cancel lookup. Neither root nor manager drains/closes
          these resources. Adapter units assert selected memory operations and fake-store Redis
          timeout/reset/refresh results, not a real tags request with those adapters or shutdown.
        sources:
          - core/server/services/tags-public/index.ts
          - core/kernel/adapter-manager/adapter-manager.ts
          - core/server/adapters/cache/MemoryCache.js
          - core/server/adapters/cache/AdapterCacheMemoryTTL.js
          - core/server/adapters/cache/memory-ttl.js
          - core/server/adapters/cache/Redis.js
          - core/server/adapters/lib/redis/AdapterCacheRedis.js
          - core/server/adapters/lib/redis/redis-store-factory.js
          - core/server/ghost-server.ts
        tests:
          - test/unit/server/adapters/cache/adapter-cache-memory-ttl.test.js
          - test/unit/server/adapters/lib/redis/adapter-cache-redis.test.js
      failures:
        summary: >-
          Source-only: dependency/adapter/listener errors reject init before service
          publication; defineService allows a later explicit retry and no rollback disposes the
          borrowed adapter. Reset is called without return/await/catch;
          synchronous throws escape emit and asynchronous rejection is unobserved. Built-in
          Redis reset instead catches/logs, so failure may retain stale entries. Pipeline hits
          return before validation/serialization/permissions; misses propagate query/cache
          failure unless the adapter swallows it. Redis loader rejection can yield undefined
          and cause a second response-loader call through the pipeline. Fake-store units do
          not cover that combined path. Tag HTTP tests assert missing identifier 400 and internal
          visibility 404, without injecting root/cache failures or proving cached authorization.
        sources:
          - core/kernel/define-service.ts
          - core/server/services/tags-public/index.ts
          - core/server/api/endpoints/tags-public.js
          - core/server/adapters/lib/redis/AdapterCacheRedis.js
          - core/server/lib/common/events.js
          - core/boot.js
        tests:
          - test/unit/server/adapters/lib/redis/adapter-cache-redis.test.js
          - test/e2e-api/content/tag-query-security.test.js
    unverified: []
    blockers:
      - >-
        Protect browse binding and shared pipeline identity together while keeping read uncached.
        Cold-boot MemoryCache HTTP now covers ordinary Admin tag-edit invalidation and the
        subsequent cached response; relevant frontend composition, early endpoint import and
        cached permission/context behavior remain untested.
      - >-
        Establish lifecycle ownership before adding reset/re-init: disabled first init is
        permanent, listeners are never removed, manager cache clearing leaves retained users,
        and no root cleanup runs after boot failure or shutdown. Current tests manually restore
        global state rather than exercise production restart.
      - >-
        Cover asynchronous invalidation, in-flight fills and background refresh across reset.
        An older MemoryCache response can be stored after reset; Redis background refresh can
        publish using a newer prefix. These are source risks, not reproduced stale HTTP responses.
      - >-
        Cover adapter construction, reset, query and cache failures through the actual pipeline,
        including swallowed Redis loader errors and repeated work. Bound/drain optional adapter
        resources without closing shared clients; selected fake-store tests do not establish this.
  themes:
    kind: directory
    disposition: migrate
    status: audited
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: complex
      modelImpact: fits
      reason: >-
        Define a stable theme-management facade across the active theme, catalogue,
        validation cache and frontend activation bridge. Initial theme activation
        deliberately logs errors without preventing Admin from starting; preserve that
        boundary and subsequent theme changes as operations on the retained owner. The
        ownership and boot-mode work is substantial, but the active theme need not be the
        immutable service instance.
      reviewedAt: f276513ba32b7114fd5cc4f7b02d3f82c0c31e3e
      sources:
        - core/server/services/themes/index.js
        - core/server/services/themes/activate.js
        - core/server/services/themes/activation-bridge.js
        - core/server/services/themes/validate.js
        - core/boot.js
      dependencies:
        complexity: complex
        reason: >-
          The catalogue, lazy ThemeStorage instance and validation-cache adapter live in different
          modules. Activation reaches bridge and Custom Theme Settings, while validation loads gscan
          lazily and reads live labs/config. Shared upload-reporting is also imported by HTTP
          middleware. These are multiple composition and ownership boundaries, not just logger
          imports.
        preparation:
          - >-
            Create one explicit composition for catalogue, storage, validation and activation,
            injecting filesystem, configuration, logging and the Custom Theme Settings/bridge
            operations.
          - >-
            Keep gscan behind a root-owned lazy loader and preserve when storage paths versus live
            validation settings are read.
          - >-
            Separate the upload-limit reporter shared with HTTP middleware from the running theme
            graph; reconsider ThemeStorage's external adapter inheritance as a storage dependency
            boundary.
        reviewedAt: 44ad405bff4a8700af2ed51f833ebc08d25dddff
        sources:
          - core/server/services/themes/index.js
          - core/server/services/themes/list.js
          - core/server/services/themes/storage.js
          - core/server/services/themes/theme-storage.js
          - core/server/services/themes/validate.js
          - core/server/services/themes/activation-bridge.js
          - core/server/services/themes/upload-size-limit-reporter.js
          - core/server/web/api/middleware/upload.js
    entryPoints:
      - themes/index.js
      - themes/activate.js
      - themes/list.js
    notes: >-
      Record the module API, shared theme list, validation adapter and frontend
      activation boundary together with file ownership. All facets are source-checked;
      tests cover selected operations, not complete initialization or cleanup contracts.
    contracts:
      exports:
        summary: >-
          Source-checked: the stable CommonJS object exposes async init,
          loadInactiveThemes and an eagerly populated api containing getJSON, activate,
          getThemeErrors, getZip, zipToFile, setFromZip, installFromGithub and destroy.
          These are captured functions, available before init without a readiness guard.
          getJSON synchronously returns a themes envelope; activation returns errors and
          warnings; getZip resolves an Express handler; zipToFile resolves path/size.
          Deep activate/list exports remain runtime entrypoints, and upload middleware
          imports the size-limit reporter. HTTP assertions exercise selected response
          shapes, not export identity or detached-method compatibility.
        sources:
          - core/server/services/themes/index.js
          - core/server/services/themes/activate.js
          - core/server/services/themes/list.js
          - core/server/services/themes/to-json.js
          - core/server/services/themes/storage.js
          - core/server/services/themes/theme-storage.js
          - core/server/services/themes/upload-size-limit-reporter.js
          - core/server/web/api/middleware/upload.js
        tests:
          - test/e2e-api/admin/themes.test.js
      construction:
        summary: >-
          Source-checked: import captures helpers, limits and function references and
          reaches the frontend bridge. The bridge obtains shared asset instances whose
          constructors read the card manifest and create/copy Admin authentication
          files; the shared rendering engine and both translation objects also exist
          before themes.init. Every init acquires cache:gscan through adapter-manager
          and loads/checks/activates the current theme. File operations separately create
          one lazy ThemeStorage with a captured content path. Each successful frontend
          activation constructs a new ActiveTheme. Loader units use real temporary
          directories and assert parsed/missing-package results; storage and validation
          units replace their collaborators and do not characterize root acquisition.
        sources:
          - core/server/services/themes/index.js
          - core/server/services/themes/validate.js
          - core/server/services/themes/storage.js
          - core/server/services/themes/theme-storage.js
          - core/server/services/themes/loader.js
          - core/server/lib/package-json/package-json.js
          - core/bridge.js
          - core/frontend/services/assets-minification/index.js
          - core/frontend/services/assets-minification/card-assets.js
          - core/frontend/services/assets-minification/admin-auth-assets.js
          - core/frontend/services/theme-engine/active.js
          - core/frontend/services/theme-engine/engine.js
          - core/frontend/services/theme-engine/i18n/index.js
          - core/frontend/services/theme-engine/i18next/index.js
          - core/kernel/adapter-manager/adapter-manager.ts
        tests:
          - test/unit/server/services/themes/loader.test.js
          - test/unit/server/services/themes/storage.test.ts
          - test/unit/server/services/themes/validate.test.js
      consumers:
        summary: >-
          Source-checked: boot and Admin themes/export controllers retain the root.
          Admin activation awaits theme activation before persisting active_theme;
          upload/install emit theme.uploaded after installation and set invalidation
          headers for overrides. Site export also reads the deep list and supplies its
          own temporary ZIP cleanup callback. The custom-settings importer retains deep
          activate and calls it without awaiting it after model writes. Frontend
          middleware, rendering, routing, assets and metadata read the active-theme
          slot through the bridge/theme engine. Real Admin HTTP asserts list contents,
          upload/delete filesystem and list changes, activation responses, copied custom
          settings and GitHub installation using Nock. The override case stubs the active
          setting. Full export HTTP extracts a nested theme ZIP and checks package.json;
          re-upload of an exported theme asserts HTTP 200, not complete file equality.
        sources:
          - core/boot.js
          - core/server/api/endpoints/themes.js
          - core/server/api/endpoints/exports.js
          - core/server/web/api/endpoints/admin/routes.js
          - core/server/data/importer/importers/data/custom-theme-settings-importer.js
          - core/server/data/exporter/site-exporter.ts
          - core/server/web/api/middleware/upload.js
          - core/frontend/web/site.js
          - core/frontend/services/theme-engine/middleware/ensure-active-theme.js
          - core/frontend/services/rendering/templates.js
          - core/frontend/services/routing/controllers/collection.js
          - core/frontend/services/routing/controllers/channel.js
          - core/frontend/web/middleware/static-theme.js
          - core/frontend/web/middleware/handle-image-sizes.js
          - core/frontend/web/middleware/error-handler.js
          - core/frontend/web/routers/serve-public-file.js
          - core/frontend/meta/asset-url.js
        tests:
          - test/e2e-api/admin/themes.test.js
          - test/e2e-api/admin/exports-download.test.js
          - test/e2e-frontend/theme-i18n.test.js
      startup:
        summary: >-
          Source-checked: boot initializes custom-theme-settings immediately before
          awaiting themes.init in initServicesForFrontend; that group currently runs
          even when frontend routing is disabled. Init loads only the active theme.
          Background startup calls loadInactiveThemes without awaiting its promise,
          before its test-environment guard; a later complete scan replaces the list.
          ActiveTheme is mounted into Express on the first frontend request, clearing
          asset/view caches and configuring the existing engine. VM boot units assert
          the inactive-load stub is called in test environments; they do not hold its
          promise or exercise active-theme init. Middleware units assert mounting an unmounted fake, skipping an already-mounted
          fake and missing-theme responses. Real HTTP exercises
          normal boot/activation/rendering, not missing/fatal-theme boot or an init barrier.
        sources:
          - core/boot.js
          - core/server/services/themes/index.js
          - core/server/services/themes/activate.js
          - core/server/services/themes/loader.js
          - core/server/services/themes/list.js
          - core/frontend/services/theme-engine/active.js
          - core/frontend/services/theme-engine/middleware/ensure-active-theme.js
        tests:
          - test/unit/boot/init-background-services.test.ts
          - test/unit/frontend/services/theme-engine/middleware.test.js
          - test/unit/frontend/services/theme-engine/active.test.js
          - test/e2e-api/admin/themes.test.js
          - test/e2e-frontend/theme-i18n.test.js
      lifetime:
        summary: >-
          Source-checked: there is no root init guard, reset or shutdown. Reinitializing
          reacquires the validation adapter and attempts to replace the active frontend object;
          the list, lazy storage and captured API functions have independent lifetimes.
          List.set deep-clones its input but get/getAll return live references, and
          list.init replaces the backing object; old references can survive replacement.
          Validation results are keyed only by theme name and destroy does not clear
          them. Adapter-manager caches instances by adapter name/class, so repeat init
          is not a guaranteed cache refresh. List units assert get/set/delete/reset
          values and validation units assert a second read skips checking; neither
          proves reference isolation, repeated root init or overlapping activation.
        sources:
          - core/server/services/themes/index.js
          - core/server/services/themes/list.js
          - core/server/services/themes/validate.js
          - core/server/services/themes/storage.js
          - core/kernel/adapter-manager/adapter-manager.ts
          - core/server/adapters/cache/MemoryCache.js
          - core/frontend/services/theme-engine/active.js
        tests:
          - test/unit/server/services/themes/list.test.ts
          - test/unit/server/services/themes/validate.test.js
      configuration:
        summary: >-
          Source-checked: init reads active_theme and skipBootChecks each call. Loaders
          read the content path each call; ThemeStorage captures it on first file use
          and translation singletons capture it at import. Validation reads labs,
          upload limits and environment per check and suppresses warnings in production;
          cached results bypass those reads. The HTTP uploader captures its compressed
          size limit at import. Activation reads the locale, derives package config and
          invalidates shared assets; bridge settings events refresh translations.
          Asset URL helpers are frozen at import, not a URL hot-reload contract.
          Validation units assert size options passed to fake gscan. Frontend HTTP
          asserts English/German output, interpolation, missing-key and English fallback
          while changing settings, after explicitly resetting both translation paths.
          These assertions do not prove production content-path/config replacement.
        sources:
          - core/server/services/themes/index.js
          - core/server/services/themes/loader.js
          - core/server/services/themes/validate.js
          - core/server/services/themes/theme-storage.js
          - core/server/web/api/middleware/upload.js
          - core/bridge.js
          - core/frontend/services/theme-engine/active.js
          - core/frontend/services/theme-engine/config/index.js
          - core/frontend/services/theme-engine/i18n/index.js
          - core/frontend/services/theme-engine/i18next/index.js
          - core/frontend/services/assets-minification/admin-auth-assets.js
          - core/shared/url-utils.ts
        tests:
          - test/unit/server/services/themes/validate.test.js
          - test/e2e-frontend/theme-i18n.test.js
      resources:
        summary: >-
          Source-checked: themes coordinates filesystem trees, extracted/downloaded
          temporary files, archive/read streams and the shared validation adapter;
          custom settings/models own DB work. Upload extraction/backup/staging cleanup
          runs after the main operation without being joined; fatal validation and
          GitHub download cleanup also detach removal. The download handler starts
          removal after piping, without waiting for stream completion or attaching
          stream error/abort cleanup. Site export owns its separate ZIP callback.
          Bridge owns raw locale/labs listeners; themes has no unsubscribe/drain or
          cancellation. GitHub uses the shared @tryghost/request DNS client, with test
          retries disabled, and sets no timeout itself. Installed gscan/ZIP sources
          expose additional extraction/archive cleanup boundaries. Exporter units
          manually destroy an archive with fake ZIP acquisition and assert eventual
          staged-file removal; normal download HTTP only checks headers/status.
        sources:
          - core/server/services/themes/storage.js
          - core/server/services/themes/theme-storage.js
          - core/server/services/themes/validate.js
          - core/server/services/themes/installer.js
          - core/server/adapters/storage/LocalStorageBase.ts
          - core/server/api/endpoints/exports.js
          - core/server/data/exporter/site-exporter.ts
          - core/server/web/api/middleware/upload.js
          - core/bridge.js
          - core/frontend/services/theme-engine/engine.js
          - core/frontend/services/theme-engine/i18n/i18n.js
          - core/frontend/services/theme-engine/i18next/theme-i18n.js
          - package.json
        tests:
          - test/unit/server/data/exporter/site-exporter.test.ts
          - test/e2e-api/admin/themes.test.js
          - test/e2e-api/admin/exports-download.test.js
      failures:
        summary: >-
          Source-checked: import/acquiring the cache can reject boot, but active-theme
          load/check/activation errors are logged and swallowed; even a fatal boot
          validation result proceeds to activation. API missing/fatal themes reject,
          while bridge engine/asset exceptions are logged and swallowed after custom
          settings activation. Upload stages before moving the live tree, and restores
          a backup only if the target is absent; later failures need not undo file,
          list, frontend or settings changes, and rollback errors can replace the
          original error. Validation units assert injected ZIP rejection, fatal-result
          predicates, cached result reuse and fallback after a throwing cache get;
          they do not test real fatal checkSafe cleanup or cache-set failure. Storage
          units assert copy-before-rename, no rename on failed copy and restore invocation
          before delete on promotion failure, using fake filesystem collaborators.
          HTTP asserts unknown-copy-theme 422 and a mocked limit's 403 before GitHub
          requests. Reporter units assert exact log/Sentry arguments for an entry limit.
        sources:
          - core/server/services/themes/index.js
          - core/server/services/themes/activate.js
          - core/server/services/themes/activation-bridge.js
          - core/server/services/themes/validate.js
          - core/server/services/themes/storage.js
          - core/server/services/themes/installer.js
          - core/server/services/themes/upload-size-limit-reporter.js
          - core/server/api/endpoints/themes.js
          - core/bridge.js
        tests:
          - test/unit/server/services/themes/validate.test.js
          - test/unit/server/services/themes/storage.test.ts
          - test/unit/server/services/themes/upload-size-limit-reporter.test.js
          - test/e2e-api/admin/themes.test.js
    unverified: []
    blockers:
      - >-
        Protect real root import/pre-init exports, cache acquisition failure, repeat
        initialization and retained deep consumers before changing construction. The
        existing operation tests do not pin these boundaries or the transitive import
        filesystem work. Preserve the distinct boot log-and-continue versus API reject
        behavior and verify the custom-settings prerequisite with held/failing work.
      - >-
        Characterize inactive-list loading and concurrent activate/upload/delete/init
        before adding lifecycle serialization or reset. Background scanning is unawaited
        and replaces the list; live references and name-only validation results can
        outlive removal/replacement. Importer activation is also unawaited and does not
        receive its caller's transaction, so import completion is not activation completion.
      - >-
        Cover settings/frontend/file partial failure boundaries before changing their
        ownership: custom settings activation precedes frontend activation, bridge errors
        can be swallowed, and the Admin setting is persisted afterward. Upload settings
        copy and promoted files are not rolled back together; restoration failure can
        replace the original error and is followed by detached backup deletion. Existing
        fake storage cases establish only their specific invocation order and failures.
      - >-
        Establish cleanup/drain ownership before moving archive or transport acquisition.
        Test actual download completion/abort/error, removal rejection and extraction
        failure; successful HTTP or manual exporter archive destruction does not cover
        themes' download stream. Installed gscan keeps extracted trees and can fail before
        returning a checked path, while cleanup uses the resolved theme path rather than
        necessarily the extraction root. ZIP compression lacks an output-stream error
        handler in the inspected dependency. These are source-derived risks, not reproduced
        regressions. No root disposal covers requests, shared adapter resources or detached work.
      - >-
        Pin configuration timing if dependency capture changes: separate loader/storage/
        translation paths, shared asset snapshots, cached warnings and live settings/labs.
        Theme-i18n HTTP resets captured paths manually; Nock GitHub cases do not exercise
        production retries, deadlines or DNS/socket disposal. Record these themes-specific
        obligations without making unrelated business fixes prerequisites for the pilot.
  tiers:
    kind: directory
    disposition: migrate
    status: audited
    migrated: true
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: straightforward
      modelImpact: fits
      reason: >-
        The boot-owned defineService root builds the repository over the Product model and
        DomainEvents, awaits its priming, builds the API over it and publishes both together as
        one service. initServices awaits it in the group with Members after constructing Gifts.
        The Members API and gift services are built before it is ready, so each reads the tiers
        API through a getter at use. The repository owns its in-memory tier state but no startup
        subscriptions or worker; no extras are needed.
      reviewedAt: c03e84fdc7e91db2310a785fbd2e5a6233da6c42
      sources:
        - core/server/services/tiers/index.ts
        - core/server/services/tiers/tier-repository.js
        - core/server/api/endpoints/tiers.js
        - core/boot.js
      dependencies:
        complexity: straightforward
        reason: >-
          Product models, event dispatch and slug generation are already supplied by the wrapper,
          and the repository is hydrated before publishing the API. Remaining NQL imports are local
          repository dependencies. Members, Gifts and Stripe checkout read the shared facade later;
          they do not require a reverse runtime service dependency inside Tiers.
        preparation:
          - >-
            Move wrapper composition to index.ts and inject the NQL operation into the production
            repository; move the test-only in-memory implementation to test support or wire its
            dependency explicitly.
          - >-
            Preserve API/repository identity and readiness for root and deep service.js consumers,
            including Members middleware and payment-event subscriptions.
        reviewedAt: 44ad405bff4a8700af2ed51f833ebc08d25dddff
        sources:
          - core/server/services/tiers/index.ts
          - core/server/services/tiers/tier-repository.js
          - test/unit/server/services/tiers/in-memory-tier-repository.ts
          - core/server/services/members/middleware.js
          - core/server/services/members/service.js
          - core/server/services/stripe-checkout-config/index.ts
    entryPoints:
      - tiers/index.ts
    notes: >-
      Migration inspected c03e84fdc7e91db2310a785fbd2e5a6233da6c42 with the characterization from
      the preceding tests-only commit. Boot owns the tier repository and API; tier data is
      cached. The members deep import of the wrapper is gone and the in-memory repository now
      sits beside the TiersAPI units. NQL and bson-objectid stay imported where used as allowed
      libraries. auditedAt and the dependency assessment retain historical provenance.
    contracts:
      exports:
        summary: >-
          The TypeScript root default-exports a defineService root with async init and no
          shutdown/reset. Its service, holding api and repository, throws IncorrectUsageError
          before initialization. The API exposes async
          browse/read/readDefaultTier/add/edit; browse returns one-page metadata, read returns
          null for a missing valid ID and readDefaultTier can return undefined. API units use
          the separate in-memory repository: they assert edited name, archived status, browse
          counts and a default-tier name, but only a truthy add/read result. Root units pin the
          service throwing before init and API operations needing the API as receiver. Missing-result shapes are source-only.
        sources:
          - core/server/services/tiers/index.ts
          - core/server/services/tiers/tiers-api.js
          - core/server/services/tiers/tier-repository.js
          - test/unit/server/services/tiers/in-memory-tier-repository.ts
        tests:
          - test/unit/server/services/tiers/index.test.ts
          - test/unit/server/services/tiers/tiers-api.test.js
      construction:
        summary: >-
          The root's factory creates TierRepository with Product and DomainEvents, and a slug
          collaborator calling Product.generateSlug. It awaits loading products with benefits,
          then builds TiersAPI and returns both for publication together. This is the only
          production construction path found for these two implementations. Deep Tier and event
          classes support that graph; the in-memory tier repository is used by API units, not
          production composition. Root units pin the benefits load, the API reading through the
          published repository, TierCreatedEvent dispatch through DomainEvents, and slugs from
          the current Product model while the repository saves through the model read at init.
        sources:
          - core/server/services/tiers/index.ts
          - core/server/services/tiers/tier-repository.js
          - core/server/services/tiers/tiers-api.js
          - core/server/services/tiers/tier.js
          - test/unit/server/services/tiers/in-memory-tier-repository.ts
        tests:
          - test/unit/server/services/tiers/index.test.ts
      consumers:
        summary: >-
          Source searches found boot, Admin/Content tier endpoints, post/page/
          preview/email-post serializers, members imports/access middleware/controllers, gifts
          and Stripe checkout previews. These consumers resolve the service's api at use; the
          Members API and gift services receive a getter that does so, and payments deep-imports
          three tier event classes. The preview callback resolves the current api.read for each
          request. Its HTTP tests assert refusal of free, absent and malformed tiers, but do not
          test replacing the tiers API. Admin HTTP assertions cover serialized browse/add/edit
          fields, visibility and null trial-day updates, plus a persisted benefit capitalization
          check. Content HTTP assertions cover active/visibility filtering. The gift delivery
          unit proves API lookup at send time with an injected wrapper whose api is assigned
          after construction. Consumer units pin per-call API reads with the API as receiver for
          both endpoints, the four post/page/preview/email-post serializers, the members
          access-cookie middleware and the Stripe Checkout readTier callback, and that the
          members API and gift services read the root's current API at each access. The members importer's tier lookups are covered
          only through the Admin members import HTTP suite; payment-event paths remain
          source-checked.
        sources:
          - core/boot.js
          - core/server/api/endpoints/tiers.js
          - core/server/api/endpoints/tiers-public.js
          - core/server/api/endpoints/utils/serializers/input/tiers.js
          - core/server/api/endpoints/utils/serializers/output/tiers.js
          - core/server/api/endpoints/utils/serializers/output/posts.js
          - core/server/api/endpoints/utils/serializers/output/pages.js
          - core/server/api/endpoints/utils/serializers/output/previews.js
          - core/server/api/endpoints/utils/serializers/output/email-posts.js
          - core/server/services/members/service.js
          - core/server/services/members/api.js
          - core/server/services/members/middleware.js
          - core/server/services/members/members-api/members-api.js
          - core/server/services/members/members-api/controllers/member-controller.js
          - core/server/services/members/members-api/controllers/router-controller.js
          - core/server/services/members/members-api/services/payments-service.js
          - core/server/services/gifts/index.ts
          - core/server/services/gifts/gift-service.ts
          - core/server/services/gifts/gift-delivery-service.ts
          - core/server/services/stripe-checkout-config/index.ts
          - core/server/services/stripe-checkout-config/preview-service.ts
        tests:
          - test/e2e-api/admin/tiers.test.js
          - test/e2e-api/content/tiers.test.js
          - test/unit/server/services/gifts/gift-delivery-service.test.ts
          - test/e2e-api/admin/stripe-checkout-preview.test.ts
          - test/e2e-api/admin/members-import.test.js
          - test/unit/server/services/tiers/consumers.test.ts
      startup:
        summary: >-
          Source establishes that initServices awaits Stripe, constructs gifts with a tiers getter,
          then awaits tiers.init alongside members.init. Tier loading must complete before that
          group and boot can finish; members and gifts deliberately hold getters before the
          service is available. defineService shares one pending initialization between
          overlapping calls, as its kernel units cover. Root units pin publication only after priming resolves, nothing published and a
          retry loading again after priming or API construction rejects, sequential init
          retaining both instances without reloading, and overlapping calls settling with an API
          over the published repository. Boot units pin gifts constructed before tiers.init
          starts, and jobs waiting for tiers.init and not starting when it rejects. HTTP fixtures
          separately call the service's repository.init after inserting fixture rows when the
          root is initialized; that refresh is not evidence that calling init again reloads
          tiers.
        sources:
          - core/boot.js
          - core/server/services/tiers/index.ts
          - core/server/services/members/members-api/members-api.js
          - core/server/services/gifts/index.ts
          - test/utils/fixture-utils.js
        tests:
          - test/unit/boot/init-services.test.ts
          - test/unit/kernel/define-service.test.ts
          - test/unit/server/services/tiers/index.test.ts
      lifetime:
        summary: >-
          Source establishes that successful sequential init calls retain the repository and API
          without reloading. The repository keeps tiers and an ID set; reads filter that cache
          and construct new Tier objects, but retain their benefits arrays, Date objects and
          ObjectIDs. Scalar edits on a read copy are separate until save, while nested mutation
          can affect cached data. External database changes are not observed until explicit
          repository.init; save caches its pre-write Tier copy without applying the returned
          model's normalization or timestamps. Default-tier selection uses the first matching
          cached row; members' name lookup takes the last. Repository units only assert add/edit
          call counts, a truthy read and no findOne; they do not establish clone isolation,
          saved values, reload or concurrent behavior.
        sources:
          - core/server/services/tiers/index.ts
          - core/server/services/tiers/tier-repository.js
          - core/server/services/tiers/tier.js
          - core/server/services/tiers/tiers-api.js
          - core/server/models/product.js
          - core/server/services/members/service.js
        tests:
          - test/unit/server/services/tiers/tier-repository.test.js
      configuration:
        summary: >-
          Tiers itself does not read operator config. Product fields become cached Tier values
          at load/save. No root hot-reload/reset is provided. Source also establishes
          Mongo-object filtering, ignored pagination/order options, paid/active default
          selection and falsy price handling: new paid zero prices become 500/5000, while an API
          edit's zero prices retain existing values. Units assert the new-tier zero defaults;
          Admin HTTP asserts invalid-price rejection and null trial becoming zero. These tests
          do not cover zero-price edits.
        sources:
          - core/server/services/tiers/index.ts
          - core/server/services/tiers/tier-repository.js
          - core/server/services/tiers/tiers-api.js
          - core/server/services/tiers/tier.js
        tests:
          - test/unit/server/services/tiers/tier.test.js
          - test/e2e-api/admin/tiers.test.js
      resources:
        summary: >-
          Source establishes retained cache/model references and borrowed model/database
          resources, with no timers, clients, subscriptions or cleanup registered by this root.
          Tier save awaits Product persistence, updates its cache and dispatches every queued
          event without clearing the queue or awaiting handlers. Members PaymentsService owns
          subscribers that create/update Stripe products/prices and their database mappings; it
          retains tier event payload references and has no subscription disposal here.
          DomainEvents catches/logs subscribed handler errors; its allSettled tracking is
          enabled by test environment at import, not a production drain. These nested completion
          and cleanup facts come from source, not the repository stub or HTTP response
          assertions.
        sources:
          - core/server/services/tiers/index.ts
          - core/server/services/tiers/tier-repository.js
          - core/server/services/tiers/tier.js
          - core/server/services/tiers/tier-created-event.js
          - core/server/services/tiers/tier-price-change-event.js
          - core/server/services/tiers/tier-name-change-event.js
          - core/server/services/tiers/tier-activated-event.js
          - core/server/services/tiers/tier-archived-event.js
          - core/server/services/members/members-api/services/payments-service.js
          - package.json
        tests: []
      failures:
        summary: >-
          Source establishes propagated repository-load/slug/validation/database failures. A
          load failure before publication leaves the root retryable. Failed row conversion
          during repository reload can retain the old store with a cleared/part-filled ID set. A
          rejected Product write prevents normal cache replacement and event dispatch, but no
          transaction joins cached tier state, tier persistence, async Stripe work and its
          mapping writes. Re-saving the same event-bearing Tier dispatches its old events again.
          Direct API edit of a missing valid ID dereferences null; malformed IDs and string
          filters reject. Admin HTTP asserts 422 responses/error snapshots for fractional,
          negative and excessive prices; API units only assert some error for free creation. The
          remaining failure/recovery paths are source-only, not demonstrated fault-injection or
          provider-completion contracts.
        sources:
          - core/server/services/tiers/index.ts
          - core/server/services/tiers/tier-repository.js
          - core/server/services/tiers/tiers-api.js
          - core/server/services/tiers/tier.js
          - core/server/services/members/members-api/services/payments-service.js
        tests:
          - test/unit/server/services/tiers/index.test.ts
          - test/unit/server/services/tiers/tiers-api.test.js
          - test/e2e-api/admin/tiers.test.js
    unverified: []
    blockers:
      - >-
        Characterize production repository clone boundaries, external-write/reload behavior,
        failed reload ID/store consistency, write races, database normalization/timestamps and
        default-tier ordering. The repository test changes the fetched object but saves the
        original; its call-count assertions do not prove edited data or cache isolation.
      - >-
        Pin missing/invalid ID results, filter/pagination behavior, zero-price edits and exact
        validation failures at the intended boundary. In-memory API tests keep original Tier
        objects and omit database/event behavior; several Tier setter rejection helpers are
        async calls that the tests neither await nor return.
      - >-
        Cover tier-event payload identity, duplicate dispatch on repeated saves and actual
        member payment subscription effects, including provider/mapping failures and shutdown.
        Existing event-presence units and HTTP tier responses do not establish Stripe
        completion, atomicity, retry safety, listener disposal or a production drain.
  tinybird:
    kind: directory
    disposition: review
    status: audited
    auditedAt: c2999f4e13da6360bceced808bb52c4a04b5ad03
    migration:
      complexity: complex
      modelImpact: may-change
      reason: >-
        The token endpoint replaces the client on every request, resetting its JWT cache,
        while Stats retains an older client and reuses its tokens. Decide the shared
        token/cache and disabled-to-enabled contract before adopting one retained instance;
        this changes observable expiry reuse as well as dependency identity. A stable
        reconfigurable facade may suffice, but simply memoizing current init would silently
        change those behaviors.
      reviewedAt: f276513ba32b7114fd5cc4f7b02d3f82c0c31e3e
      sources:
        - core/server/services/tinybird/tinybird-service-wrapper.js
        - core/server/services/tinybird/tinybird-service.js
        - core/server/services/stats/stats-service.js
        - core/server/api/endpoints/tinybird.js
        - core/server/services/stats/utils/tinybird.ts
      dependencies:
        complexity: straightforward
        reason: >-
          The implementation receives a configuration object and captures the selected site identity
          and mode flags, with only the JWT library imported directly. Dependency wiring is small,
          but both the token endpoint and Stats call init, which replaces the shared client; the
          endpoint does so on every request. Client replacement, configuration refresh and
          token-cache lifetime remain separate unresolved lifecycle decisions.
        preparation:
          - >-
            Inject JWT signing/verification from index.ts while preserving the distinction between
            the configuration object and the identity/mode flags captured by each client.
          - >-
            Characterize per-request replacement, Stats-held client identity, configuration refresh
            and the unconfigured state before choosing a retained facade and boot-owned
            initialization; do not silently lengthen token-cache lifetime.
        reviewedAt: 44ad405bff4a8700af2ed51f833ebc08d25dddff
        sources:
          - core/server/services/tinybird/tinybird-service-wrapper.js
          - core/server/services/tinybird/tinybird-service.js
          - core/server/services/stats/stats-service.js
          - core/server/services/automations/automations-api.ts
          - core/server/api/endpoints/tinybird.js
    entryPoints:
      - tinybird/index.js
    notes: >-
      Review ownership and client lifetime before migration. Allowed pipes and site scoping are
      application policy across traffic, gifts and automation analytics. Reusable provider/JWT
      mechanics could become library support; they are not Kernel
      machinery. An optional token service is published through static methods. Preserve the
      different lifetimes of request-time initialization, retained stats clients and automation
      lookups.
    contracts:
      exports:
        summary: >-
          CommonJS index re-exports the wrapper class, with static synchronous init/reset and
          mutable instance; instance starts undefined, and disabled init/reset assigns null.
          Static methods reference the wrapper class directly rather than their receiver;
          both return undefined. The deep implementation is a constructible class exposing
          synchronous getToken, _generateToken and _isJWTExpired, with mutable configuration,
          capability flags and token-cache fields. getToken returns token/optional numeric exp
          or null. Units assert selected token shapes and static-token values; HTTP asserts
          the serialized token/null shapes and ISO expiration. Exact root exports, unbound
          static methods and reset behavior are source-only.
        sources:
          - core/server/services/tinybird/index.js
          - core/server/services/tinybird/tinybird-service-wrapper.js
          - core/server/services/tinybird/tinybird-service.js
          - core/server/api/endpoints/tinybird.js
          - core/server/api/endpoints/utils/serializers/output/tinybird.js
        tests:
          - test/unit/server/services/tinybird/tinybird-service.test.js
          - test/e2e-api/admin/tinybird.test.js
      construction:
        summary: >-
          Every configured init constructs a new TinybirdService from current tinybird config
          and settings site_uuid; no guard or shared instance reuse exists. Only the wrapper
          constructs it in production; units construct it directly. Construction captures
          flags and site ID, retains the config subtree and starts empty token-cache fields,
          without signing or contacting Tinybird. The Stats root conditionally initializes it
          through initializeTinybirdForStats, then creates a separate query client for web
          analytics using the published instance. Stats root units stub init and assert its gate
          plus client presence; the
          retained-consumer test uses the real root and asserts replacement identity. Nested
          construction and absence of provider I/O at init are otherwise source-established.
        sources:
          - core/server/services/tinybird/tinybird-service-wrapper.js
          - core/server/services/tinybird/tinybird-service.js
          - core/server/services/stats/index.ts
          - core/server/services/stats/utils/tinybird.ts
        tests:
          - test/unit/server/services/stats/index.test.ts
          - test/unit/server/services/tinybird/consumer-configuration.test.ts
      consumers:
        summary: >-
          Three production root consumers were found: the Admin token endpoint initializes
          on each request, the Stats root initializes and captures instance, and the
          automations API reads the current instance without initializing it. Stats passes
          its query client to posts/content statistics; automations builds clients for browse,
          performance, runs and member search. No production deep implementation constructor
          or wrapper-module import was found beyond the barrel. Token HTTP tests assert owner/
          admin success and unauthenticated/editor denial. Automation HTTP tests exercise
          real token composition and intercepted query responses for browse/performance/member
          search; run tests replace the token service. These establish selected HTTP results, not
          provider acceptance, every consumer or frontend token-refresh behavior.
        sources:
          - core/server/api/endpoints/tinybird.js
          - core/server/web/api/endpoints/admin/routes.js
          - core/server/api/endpoints/utils/serializers/output/tinybird.js
          - core/server/services/stats/index.ts
          - core/server/services/stats/utils/tinybird.ts
          - core/server/services/stats/posts-stats-service.js
          - core/server/services/stats/content-stats-service.js
          - core/server/services/automations/automations-api.ts
          - core/server/services/automations/tinybird-automation-stats.ts
          - core/server/services/automations/automation-member-search.ts
        tests:
          - test/e2e-api/admin/tinybird.test.js
          - test/e2e-api/admin/automations.test.ts
          - test/e2e-api/admin/automation-performance-stats.test.ts
          - test/e2e-api/admin/automation-runs.test.ts
          - test/e2e-api/admin/automation-member-search.test.ts
      startup:
        summary: >-
          Boot has no direct Tinybird init; its awaited stats.init calls initializeTinybirdForStats.
          That initializes Tinybird when web_analytics_enabled or tinybird:stats config is
          truthy. The query client itself is created only for web analytics. The wrapper
          requires settings site_uuid even
          when a stats.id override exists, and requires only truthy root config, not a usable
          token/endpoint. Automations checks stats config and needs an already-published
          token service; it does not initialize the root when configuration becomes available.
          Factory units cover three gate combinations with stubbed init. An HTTP case resets
          Tinybird and calls the real stats factory with web
          analytics disabled, then asserts remote browse stats and no SQL run-stat query;
          it does not prove complete boot ordering or later gate transitions.
        sources:
          - core/boot.js
          - core/server/services/stats/index.ts
          - core/server/services/stats/utils/tinybird.ts
          - core/server/services/tinybird/tinybird-service-wrapper.js
          - core/server/services/automations/automations-api.ts
        tests:
          - test/unit/server/services/stats/index.test.ts
          - test/e2e-api/admin/automations.test.ts
      lifetime:
        summary: >-
          Source establishes that root re-init/reset changes only the published reference,
          without disposing or revoking retained instances/tokens. Each implementation caches
          one JWT and expiration, refreshing on use when verification fails or less than five
          minutes remain. Later name/lifetime options are ignored while that cached token is
          usable; endpoint re-init discards the cache each request. Stats retains its original
          token service while rereading endpoint/site configuration, whereas a new stats client
          captures the new instance. The consumer test asserts that mixed lifetime with fake
          HTTP. Clock-controlled units assert repeated token equality and different tokens
          after 176/180 minutes; equality alone does not prove signing was skipped. Automation
          lookup after replacement and outstanding-query/reset behavior remain source-only.
        sources:
          - core/server/services/tinybird/tinybird-service-wrapper.js
          - core/server/services/tinybird/tinybird-service.js
          - core/server/services/stats/stats-service.js
          - core/server/services/stats/utils/tinybird.ts
          - core/server/api/endpoints/tinybird.js
          - core/server/services/automations/automations-api.ts
        tests:
          - test/unit/server/services/tinybird/tinybird-service.test.js
          - test/unit/server/services/tinybird/consumer-configuration.test.ts
      configuration:
        summary: >-
          Site ID (stats.id or settings site_uuid) and JWT/local/static capability flags are
          captured at construction; token reads/signing use the retained config subtree.
          JWT takes precedence over local, then legacy stats token; local-enabled can return
          an undefined token without falling back. getToken defaults to 180 minutes, direct
          _generateToken to 60; signing includes a fixed pipe allowlist scoped to captured site
          ID. Request clients reread config/settings facades, including local endpoint/version.
          Immutable config replacement does not update an existing service's captured subtree.
          HTTP tests replace test config through disabled/key-A/key-B/disabled transitions and
          verify signatures/workspaces/ISO expiry; consumer units cover missing-site recovery.
          Scope units check each returned scope's type/site and three required resources, not
          the exact allowlist. Test setters and mutable unit config do not prove hot reload.
        sources:
          - core/server/services/tinybird/tinybird-service-wrapper.js
          - core/server/services/tinybird/tinybird-service.js
          - core/server/services/stats/utils/tinybird.ts
          - core/server/api/endpoints/tinybird.js
          - core/shared/config/loader.ts
        tests:
          - test/unit/server/services/tinybird/tinybird-service.test.js
          - test/unit/server/services/tinybird/consumer-configuration.test.ts
          - test/e2e-api/admin/tinybird.test.js
      resources:
        summary: >-
          The token root owns configuration references and an in-memory JWT cache only: it
          has no database, provider client, event subscription, timer, refresh job or shutdown.
          Its stats/automation consumers borrow request-external, whose shared HTTP/HTTPS
          keep-alive agents outlive a query client. Query construction asks for a 35-second
          request timeout and zero retries; POST search additionally creates a timeout AbortSignal.
          No root reset drains/cancels those requests or closes shared agents. Client units
          assert request options and signal presence with fake HTTP, not elapsed timeout or
          socket release. A member-search HTTP case asserts a delayed ten-second intercepted
          response fails in less than five seconds, not an exact three-second deadline or
          socket cleanup. A browse case substitutes plain Got to bypass request-external's
          test retry hook and asserts the first failure falls back without consuming a second
          interceptor. The shared transport is replaced in that retry case; request-external's
          normal test hook changes request timeout to five seconds. These tests do not
          establish production timing/cleanup behavior.
        sources:
          - core/server/services/tinybird/tinybird-service-wrapper.js
          - core/server/services/tinybird/tinybird-service.js
          - core/server/services/stats/utils/tinybird.ts
          - core/server/services/automations/automations-api.ts
          - core/server/services/automations/automation-member-search.ts
          - core/server/lib/request-external.js
        tests:
          - test/unit/server/services/stats/utils/tinybird.test.js
          - test/e2e-api/admin/automations.test.ts
          - test/e2e-api/admin/automation-member-search.test.ts
      failures:
        summary: >-
          Missing config/site logs and publishes null; other initialization/signing errors
          propagate. JWT verification errors mean expired and trigger regeneration, with no
          fallback to static credentials if signing fails. Query build errors occur before
          fetch's catch, while caught request/parse errors log and return null. Automations
          catches preparation/token failures: browse falls back to database stats, but performance/
          runs report failure when their client/data is unavailable. Stats consumers separately
          catch query failures and use empty/zero results. Unit tests assert invalid-JWT expiry
          and fake HTTP/JSON failures returning null. Browse HTTP tests assert database fallback
          for missing configuration/token, empty local tokens, signing stubs and intercepted
          failed/malformed responses; performance/runs assert selected 500/no-SQL-fallback
          paths. Real signing-constructor faults, cleanup and provider rejection remain uncovered.
        sources:
          - core/server/services/tinybird/tinybird-service-wrapper.js
          - core/server/services/tinybird/tinybird-service.js
          - core/server/services/stats/utils/tinybird.ts
          - core/server/services/stats/posts-stats-service.js
          - core/server/services/stats/content-stats-service.js
          - core/server/services/automations/automations-api.ts
          - core/server/services/automations/tinybird-automation-stats.ts
        tests:
          - test/unit/server/services/tinybird/tinybird-service.test.js
          - test/unit/server/services/stats/utils/tinybird.test.js
          - test/e2e-api/admin/automations.test.ts
          - test/e2e-api/admin/automation-performance-stats.test.ts
          - test/e2e-api/admin/automation-runs.test.ts
    unverified: []
    blockers:
      - >-
        Pin root/barrel compatibility, init/reset return and receiver behavior, repeated
        construction and disabled/failed initialization with real boot consumers. Cover
        analytics gates changing after initial stats boot and settings readiness, including
        missing settings site_uuid when a configured site override exists.
      - >-
        Decide how migration preserves retained stats tokens alongside reread endpoint/site
        values and automation's current-instance lookup. Existing replacement tests use fake
        HTTP; add the relevant consumer paths before changing publication or reset semantics.
      - >-
        Protect exact token precedence, scope allowlist, captured site/workspace, cache signing
        counts, default lifetimes, the exact refresh boundary and custom options on a cached
        token. Define incomplete/local credentials and real signing failure behavior; current
        equality and selected-scope assertions are not complete cache/authentication contracts.
      - >-
        Cover query construction faults through retained stats consumers, and distinguish
        empty valid data from unavailability across stats and automations. Preserve browse's
        database fallback versus performance/run failures; intercepted HTTP cannot establish
        provider acceptance, production deadlines or shared transport disposal on stop/reset.
  tinybird-sync:
    kind: directory
    disposition: review
    status: audited
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: complex
      modelImpact: fits
      reason: >-
        A retained sync instance fits defineService, but ownership is undecided. Automations
        defines the current disclosure targets; the sync capability also owns ingestion,
        durable watermarks and recurring registration. Decide whether it belongs wholly to
        Automations or remains a separate integration before migrating its composition.
      reviewedAt: 59c26cfc276946844e0694416ead8549eb637479
      sources:
        - core/server/services/tinybird-sync/index.ts
        - core/server/services/tinybird-sync/tinybird-sync-service.ts
        - core/server/services/tinybird-sync/sync-table-to-tinybird.ts
        - core/server/services/tinybird-sync/get-ingest-config.ts
        - core/server/services/jobs-service/register-job-handlers.ts
        - core/boot.js
      dependencies:
        complexity: complex
        reason: >-
          Factory dependencies are already explicit, but the owner of the target list,
          ingestion configuration and durable sync state must be settled together. A single
          current feature dataset does not establish ownership of the entire integration.
          After that decision, dependency wiring remains bounded and does not reveal a
          defineService extension.
        preparation:
          - >-
            Decide whether Automations owns the whole sync pipeline or supplies disclosure
            targets to a retained sync capability; keep provider transport and watermark
            responsibilities explicit at the chosen boundary.
          - >-
            Wire or allowlist the pure cron helper, preserve live ingest configuration,
            and keep job registration separate from construction with retry after failure.
        reviewedAt: 59c26cfc276946844e0694416ead8549eb637479
        sources:
          - core/server/services/tinybird-sync/index.ts
          - core/server/services/tinybird-sync/tinybird-sync-service.ts
          - core/server/services/tinybird-sync/sync-table-to-tinybird.ts
          - core/server/services/tinybird-sync/get-ingest-config.ts
          - core/server/services/jobs-service/cron.ts
    entryPoints:
      - tinybird-sync/index.ts
    notes: >-
      Ownership remains under review. Automations defines the current table and column
      disclosure policy, while this capability owns incremental replication, durable watermarks,
      ingestion configuration and recurring registration. Decide whether Automations owns the
      complete pipeline or supplies targets to a retained sync service. One current feature
      dataset does not establish the final owner. Preserve live configuration, cursor, retry
      and shutdown contracts. No source has moved.
    contracts:
      exports:
        summary: >-
          Named TypeScript exports scheduleJob and sync are receiver-independent async functions
          from one eagerly created closure, both resolving undefined. There is no public instance,
          init, reset or shutdown. The deep factory returns those two functions; sync helpers and
          the empty TinybirdSyncJob subclass are support, not separate application compositions.
          Factory units invoke the returned functions, but never import the real root; exact root
          shape, unbound calls and import identity are source-only.
        sources:
          - core/server/services/tinybird-sync/index.ts
          - core/server/services/tinybird-sync/tinybird-sync-service.ts
          - core/server/services/tinybird-sync/jobs/tinybird-sync-job.ts
        tests:
          - test/unit/server/services/tinybird-sync/index.test.ts
      construction:
        summary: >-
          The root calls createTinybirdSyncService at import with config/settings/logging
          facades, the current shared knex, Math.random and global fetch references, and date/ID
          factories. Construction only creates the scheduled flag and closures; no sync or
          recurring job begins until called. Each sync with valid ingest config launches both table helpers in
          parallel with one resolved ingest configuration; each helper pages its own cursor and
          serially posts its chunks. Production construction is only the root. Factory units
          use injected dependencies, SQLite and fake fetch; no real root composition test exists.
        sources:
          - core/server/services/tinybird-sync/index.ts
          - core/server/services/tinybird-sync/tinybird-sync-service.ts
          - core/server/services/tinybird-sync/sync-table-to-tinybird.ts
          - core/server/data/db/index.js
        tests:
          - test/unit/server/services/tinybird-sync/index.test.ts
      consumers:
        summary: >-
          Boot imports the root for job-handler registration and later recurring scheduling.
          The handler awaits sync through the retained root namespace and declares a dedicated
          tinybird-sync queue with concurrency one. No runtime direct factory/helper consumer
          or HTTP/CLI entry was found. Handler units assert one exact injected sync call and
          the queue declaration, not a real queued upload or deferred completion. The background
          boot harness asserts scheduling is reached after sibling email-analytics registration
          failures, using a stub root. Persisted cursor data also appears in the exporter backup
          table list; the exporter test asserts its output key, not a populated cursor round-trip.
        sources:
          - core/boot.js
          - core/server/services/jobs-service/register-job-handlers.ts
          - core/server/services/tinybird-sync/jobs/tinybird-sync-job.ts
          - core/server/data/exporter/table-lists.js
        tests:
          - test/unit/server/services/jobs-service/register-job-handlers.test.ts
          - test/unit/boot/init-background-services.test.ts
          - test/integration/exporter/exporter.test.js
      startup:
        summary: >-
          Normal boot prepares database/settings, imports this root during handler registration,
          then awaits jobs-service start. After reporting ready, it starts background setup
          without awaiting it; test environments skip recurring setup. Tinybird scheduling
          follows awaited activitypub.init, so an earlier rejection can prevent reaching it.
          Valid ingest configuration permits registration and sync without a labs gate.
          Factory units assert the fixed random cron, job class and missing-config no-op;
          the stub boot test
          proves reachability only, not complete startup readiness or failure ordering.
        sources:
          - core/boot.js
          - core/server/services/tinybird-sync/tinybird-sync-service.ts
          - core/server/services/tinybird-sync/get-ingest-config.ts
          - core/server/services/jobs-service/cron.ts
          - core/server/services/jobs-service/jobs-service.ts
        tests:
          - test/unit/server/services/tinybird-sync/index.test.ts
          - test/unit/boot/init-background-services.test.ts
      lifetime:
        summary: >-
          The scheduled flag is set before registration; a second call rejects, while a rejected
          scheduleRecurring clears it for retry. Successful registration has no reset. Default
          backend shutdown removes recurring timers, so same-process restart can retain a true
          root flag without a timer; direct sync calls also have no overlap guard. Scheduled
          deliveries are serialized only as strongly as the selected jobs backend supports,
          per process for the default backend. Watermarks persist by table_name alone, survive
          new closures and are not reset for changed site/destination. SQLite tests assert
          cursor ties across batches/later invocations; factory tests cover completed duplicate
          registration and backend rejection/retry, not pending races, root restart or overlap.
        sources:
          - core/server/services/tinybird-sync/tinybird-sync-service.ts
          - core/server/services/tinybird-sync/sync-table-to-tinybird.ts
          - core/server/services/jobs-service/index.ts
          - core/server/services/jobs-service/jobs-service.ts
          - core/server/adapters/jobs/InMemoryJobsBackend.ts
          - core/server/data/schema/schema.js
        tests:
          - test/unit/server/services/tinybird-sync/index.test.ts
          - test/unit/server/services/tinybird-sync/sync-table-to-tinybird.test.ts
      configuration:
        summary: >-
          scheduleJob and sync reread the captured config/settings facades. A pass retains
          its resolved endpoint, auth and site. Ingest requires a parseable
          tracker URL containing /.ghost/analytics/, a nonempty sync auth key and a configured
          stats.id or settings site UUID. It rewrites the path and remaps development loopback
          hosts to the gateway. Factory limits are 5000 rows, 9 MiB/1000 messages per chunk and
          a five-minute request signal; each table fixes its cutoff at now minus sixty seconds.
          Helper tests assert selected URL rewrites, site fallback and invalid URL/key rejection.
          SQLite/fake-fetch tests assert selected payload/cursor/chunk behavior; timeout coverage
          spies on AbortSignal.timeout only. Fixture-field inclusion tests are not an exact
          allowlist assertion. Immutable config and captured fetch/knex are not hot reload tests.
        sources:
          - core/server/services/tinybird-sync/index.ts
          - core/server/services/tinybird-sync/get-ingest-config.ts
          - core/server/services/tinybird-sync/tinybird-sync-service.ts
          - core/server/services/tinybird-sync/sync-table-to-tinybird.ts
          - core/server/lib/db-types/date.ts
          - core/shared/config/loader.ts
        tests:
          - test/unit/server/services/tinybird-sync/get-ingest-config.test.ts
          - test/unit/server/services/tinybird-sync/index.test.ts
          - test/unit/server/services/tinybird-sync/sync-table-to-tinybird.test.ts
          - test/unit/server/services/tinybird-sync/sync-targets.test.ts
      resources:
        summary: >-
          The root borrows knex and native global fetch, owns only closure state, and supplies
          jobs-service with recurring work. The default backend owns timers/queues and bounded
          draining; boot registers its shutdown, which does not abort active handlers after
          its deadline. Sync has no stop signal or disposer and creates timeout signals per
          upload. Successful fetch responses are not explicitly consumed/cancelled; transport
          lifetime remains with native fetch. Cursor writes use shared database connections.
          Generic backend tests assert recurring ticks stop and a released delivery can finish
          after shutdown; they do not exercise an upload or cursor write during shutdown.
          Sync tests use fake fetch and establish request contents/options, not transport cleanup.
        sources:
          - core/server/services/tinybird-sync/index.ts
          - core/server/services/tinybird-sync/sync-table-to-tinybird.ts
          - core/server/services/jobs-service/jobs-service.ts
          - core/server/adapters/jobs/InMemoryJobsBackend.ts
          - core/boot.js
        tests:
          - test/unit/server/adapters/jobs/in-memory-jobs-backend.test.ts
          - test/unit/server/services/tinybird-sync/index.test.ts
          - test/unit/server/services/tinybird-sync/sync-table-to-tinybird.test.ts
      failures:
        summary: >-
          Incomplete ingest config resolves without work. Individual table
          failures settle independently, then log without rejecting sync; a failed table can
          therefore coexist with a completed recurring delivery and retry on a later tick.
          Config or final error-logging exceptions still reject. Registration failure
          propagates and resets the flag, but logging/random exceptions after setting it
          and before the try do not. Boot catches scheduling failures only once reached.
          A helper advances its watermark only after all batch chunks succeed; accepted chunks
          can replay if a later chunk or watermark write fails. There is no upload/database
          transaction, locking, tombstone sync or guarantee against commits delayed beyond the
          safety lag. SQLite tests assert a missing table is logged without rejecting and a
          503 preserves the prior last_synced_id; they do not cover partial-chunk/write failures,
          long transactions, concurrent passes, native aborts or provider acceptance.
        sources:
          - core/server/services/tinybird-sync/tinybird-sync-service.ts
          - core/server/services/tinybird-sync/sync-table-to-tinybird.ts
          - core/server/services/jobs-service/jobs-service.ts
          - core/server/adapters/jobs/InMemoryJobsBackend.ts
          - core/boot.js
        tests:
          - test/unit/server/services/tinybird-sync/index.test.ts
          - test/unit/server/services/tinybird-sync/sync-table-to-tinybird.test.ts
    unverified: []
    blockers:
      - >-
        Add real-root and boot lifecycle coverage before changing eager composition. Preserve
        captured knex/fetch and receiver-independent exports. Resolve successful scheduling
        state surviving cleared backend timers on restart, pending registration/shutdown races,
        and startup being skipped after an earlier background-service failure.
      - >-
        Cover pass overlap, per-process versus cross-process serialization, late commits beyond
        the safety lag, updates around cursor ties and partial delivery/watermark write failure.
        Define replay, remote deduplication and deletion expectations; SQLite/fake-fetch cursor
        tests do not establish MySQL concurrency or exactly-once delivery.
      - >-
        Define progress when endpoint/site configuration changes because watermarks are keyed
        only by table. Pin missing-site/invalid-override fallback, exact field allowlists for
        both targets, changing cutoffs across passes, production batch/byte limits and a single
        oversized event. Current chunking sends an individually oversized line rather than
        rejecting it.
      - >-
        Cover upload cancellation and cursor writes during bounded shutdown, response-body
        disposal, real request deadlines and native-fetch failures. Retain table-failure
        continuation and registration retry, including exceptions before its protected await;
        current units do not prove provider ingestion or clean resource release.
  update-check:
    kind: directory
    disposition: review
    status: audited
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: complex
      modelImpact: fits
      reason: >-
        Ownership remains undecided between a standalone update-check capability and
        Notifications-owned remote notice ingestion. Per-invocation construction does not
        establish that this is non-service support. Either owner can expose a repeatable
        check operation without changing defineService; preserve live configuration,
        persisted due-date gating and caller-specific error policy when choosing its lifetime.
      reviewedAt: 59c26cfc276946844e0694416ead8549eb637479
      sources:
        - core/server/services/update-check/index.js
        - core/server/services/update-check/update-check-service.js
        - core/server/services/update-check/jobs/update-check-job.ts
        - core/server/services/notifications/notifications.js
        - core/server/services/jobs-service/register-job-handlers.ts
        - core/boot.js
      dependencies:
        complexity: complex
        reason: >-
          The root assembles settings, users and notifications API operations, request and
          mail collaborators. Choose the capability owner and composition boundary before
          moving those dependencies; creating a new graph per invocation does not justify
          relocation to server/lib or skipping initialization assessment.
        preparation:
          - >-
            Decide whether update-check remains a separate integration or becomes private
            Notifications support, keeping request policy, group filtering and alert delivery
            with that owner and job registration in boot.
          - >-
            Preserve per-invocation configuration and error modes, live due-date reads and
            mail dependency readiness; keep repeated checking separate from initialization.
        reviewedAt: 59c26cfc276946844e0694416ead8549eb637479
        sources:
          - core/server/services/update-check/index.js
          - core/server/services/update-check/update-check-service.js
          - core/server/services/update-check/jobs/update-check-job.ts
          - core/server/services/notifications/notification-email.ts
          - core/server/services/jobs-service/register-job-handlers.ts
          - core/boot.js
    entryPoints:
      - update-check/index.js
    notes: >-
      Ownership needs review: the checker owns remote request policy, persisted check timing,
      notification-group filtering and administrator alert delivery. Notifications is a possible
      owner, but consuming its API does not establish ownership. Per-call construction alone
      is not a reason to relocate. Boot owns job registration and scheduling; preserve a
      repeatable check operation and its dependency, readiness and error contracts at either
      destination. No source has moved.
    contracts:
      exports:
        summary: >-
          CommonJS exports an async function with scheduleJobs(jobsService). It exposes no
          checker instance, init or shutdown. The implementation file exports the checker
          constructor; the TypeScript default job export has type update-check and an empty
          serializable payload.
        sources:
          - core/server/services/update-check/index.js
          - core/server/services/update-check/update-check-service.js
          - core/server/services/update-check/jobs/update-check-job.ts
        tests:
          - test/unit/server/services/update-check/schedule-update-check.test.ts
          - test/unit/server/services/update-check/jobs/update-check-jobs.test.ts
      construction:
        summary: >-
          Every invocation passing the environment/force gate constructs GhostMailer, its
          configured transport, NotificationEmailService and UpdateCheckService. The persisted
          next_update_check gate runs afterwards, so an early check still constructs these
          objects. The checker captures internal settings read/edit, users browse and
          notifications add pipeline functions. Only the root constructs the checker in
          runtime code; unit tests also construct it directly.
        sources:
          - core/server/services/update-check/index.js
          - core/server/services/update-check/update-check-service.js
          - core/server/lib/mail/ghost-mailer.js
          - core/server/api/endpoints/index.js
        tests: []
      consumers:
        summary: >-
          Boot calls scheduleJobs and centrally registers UpdateCheckJob with a handler that
          awaits the root with rethrowErrors true on the shared default jobs lane. The current
          in-memory backend invokes that handler in-process; no separate update-check worker,
          CLI or HTTP consumer was found. The checker itself calls API pipelines with internal
          context to read/write the due date, resolve active Owner/Administrator recipients
          for alerts and store notifications. Alerts await notification email before storage.
        sources:
          - core/boot.js
          - core/server/services/jobs-service/register-job-handlers.ts
          - core/server/services/jobs-service/jobs-service.ts
          - core/server/adapters/jobs/InMemoryJobsBackend.ts
          - core/server/services/update-check/update-check-service.js
          - core/server/api/endpoints/settings.js
          - core/server/api/endpoints/users.js
          - core/server/api/endpoints/notifications.js
        tests:
          - test/unit/server/services/jobs-service/register-job-handlers.test.ts
          - test/unit/server/services/update-check.test.js
          - test/integration/jobs/update-check.test.ts
      startup:
        summary: >-
          Foreground boot registers the handler and starts jobs before mounting the app and
          signalling readiness. Unawaited background startup skips test environments; otherwise
          it reaches update-check after recovery, other schedules and ActivityPub. scheduleJobs
          awaits a random daily recurring registration before optionally enqueueing a forced
          boot check. Enqueue completion does not wait for checking. There is no root readiness
          guard; direct calls rely on initialized API/settings and mail dependencies.
        sources:
          - core/boot.js
          - core/server/services/update-check/index.js
          - core/server/services/jobs-service/jobs-service.ts
          - core/server/lib/mail/ghost-mailer.js
        tests:
          - test/unit/server/services/update-check/schedule-update-check.test.ts
          - test/unit/boot/init-background-services.test.ts
          - test/integration/jobs/update-check.test.ts
      lifetime:
        summary: >-
          The root retains no checker or local scheduling flag. Repeated calls create fresh
          collaborators; concurrent checks have no local exclusion and share the persisted due
          date and notifications. Repeated scheduling generates another cron and repeats a
          forced enqueue. The in-memory jobs backend keeps the first recurring schedule per
          type until shutdown clears it. Jobs init reuses its instance and clears handlers;
          a subsequent boot must register handlers, restart jobs and schedule again.
        sources:
          - core/server/services/update-check/index.js
          - core/server/services/update-check/update-check-service.js
          - core/server/services/jobs-service/index.ts
          - core/server/services/jobs-service/jobs-service.ts
          - core/server/adapters/jobs/InMemoryJobsBackend.ts
          - core/boot.js
        tests:
          - test/unit/server/adapters/jobs/in-memory-jobs-backend.test.ts
          - test/unit/server/services/jobs-service/jobs-service.test.ts
      configuration:
        summary: >-
          Invocation defaults reread forceUpdate and endpoint; explicit options override them.
          Without forceUpdate only config environments production/development pass. A checker
          captures endpoint, notification groups, site URL, version and error policy, then
          reads next_update_check per check; force also bypasses that due-date gate. Requests
          use GET with only ghost_version in the query and a one-second request timeout; this
          root has no privacy flag gate. Custom notifications match configured groups plus all.
          Mail transport/options are read at construction; recipients and mail template site
          details are resolved when sending. scheduleJobs separately rereads forceUpdate.
        sources:
          - core/server/services/update-check/index.js
          - core/server/services/update-check/update-check-service.js
          - core/shared/config/helpers.ts
          - core/shared/config/defaults.json
          - core/server/lib/mail/ghost-mailer.js
          - core/server/lib/mail/index.js
          - core/server/lib/mail/email-content-generator.js
          - core/server/services/notifications/notification-email.ts
        tests:
          - test/unit/server/services/update-check.test.js
          - test/unit/server/services/update-check/schedule-update-check.test.ts
          - test/unit/shared/config/helpers.test.ts
          - test/integration/jobs/update-check.test.ts
      resources:
        summary: >-
          Jobs owns recurring timers and queued deliveries. Boot registers jobs shutdown with
          the configured timeout; the in-memory backend clears timers, drops waiting jobs and
          waits for active handlers until that deadline, without cancelling their requests or
          sends. The root awaits check, which normally awaits request, settings writes and
          sequential notification/email work. Each allowed invocation owns a mail transport,
          but neither the root nor GhostMailer exposes disposal. The request function and
          template generator are shared imports; no update-check cancellation hook exists.
        sources:
          - core/boot.js
          - core/server/services/jobs-service/jobs-service.ts
          - core/server/adapters/jobs/InMemoryJobsBackend.ts
          - core/server/services/update-check/index.js
          - core/server/services/update-check/update-check-service.js
          - core/server/lib/mail/ghost-mailer.js
          - core/server/lib/mail/index.js
          - core/server/services/notifications/notification-email.ts
        tests:
          - test/unit/server/adapters/jobs/in-memory-jobs-backend.test.ts
          - test/unit/server/services/jobs-service/jobs-service.test.ts
          - test/unit/server/services/notifications/notification-email.test.ts
      failures:
        summary: >-
          Scheduling failures reject to boot, which logs and continues to later background
          services. A request 404 becomes an empty result with a next-day due date. Other
          check errors call updateCheckError: it starts an unawaited next-day settings edit,
          annotates/logs the error and rethrows only when configured. Default alert recipient
          or send errors log and continue to notification storage; job mode rethrows and stops
          that check. Successful responses write the due date before notification work, so a
          later failure can leave partial state. Constructor failures bypass the checker
          error handler. Jobs logs/reports
          propagated delivery errors; its in-memory backend catches them and continues other
          work without retrying that delivery.
        sources:
          - core/boot.js
          - core/server/services/update-check/index.js
          - core/server/services/update-check/update-check-service.js
          - core/server/services/jobs-service/register-job-handlers.ts
          - core/server/services/jobs-service/jobs-service.ts
          - core/server/adapters/jobs/InMemoryJobsBackend.ts
        tests:
          - test/unit/server/services/update-check.test.js
          - test/unit/server/services/jobs-service/jobs-service.test.ts
          - test/unit/server/adapters/jobs/in-memory-jobs-backend.test.ts
    unverified: []
    blockers:
      - >-
        Choose the application/jobs capability owner and preserve the internal API pipeline
        behavior before changing composition. Current direct checker tests do not establish
        root export shape, construction count, pre-init behavior, environment/force gates,
        explicit overrides or live configuration changes end-to-end.
      - >-
        Add update-check-specific repeated/concurrent scheduling and restart/drain contracts.
        Generic backend tests cover first-registration wins, shutdown and queue restart, not
        this root's forced dispatches, actual recurring checks or overlapping due-date writes.
      - >-
        Characterize recurring registration and forced enqueue failures, including the partial
        state when enqueue fails after scheduling succeeds. Existing boot coverage continues
        after email analytics errors, not update-check scheduling errors or earlier ActivityPub
        failure; daily cron tests do not bound seconds/minutes or assert registration ordering.
      - >-
        updateCheckError neither awaits nor catches settings.edit. Its rejection can escape
        after check/job completion and is not drained as part of the handler. Decide and test
        this behavior before migration; helper tests use resolving stubs and only check calls,
        error annotation and rethrow, not timestamp persistence or failed writes.
      - >-
        Characterize request timeout/404/malformed/error responses, settings and notification
        write failures, invalid notification groups, and alert lookup/render/send failures in
        default and job modes. Successful job integration and helper-level rethrow tests do not
        prove that the real handler reports a failed update check or preserves later work.
      - >-
        Decide ownership and disposal for per-check mail transports and in-flight HTTP/email
        work, including shutdown timeout and late completion. Existing tests do not prove
        transport release or cancellation, and the one-second request option is not evidence
        of an overall deadline or of dependency-level retry/socket cleanup behavior.
  upgrades:
    kind: directory
    disposition: migrate
    status: auditing
    migrated: true
    migration:
      complexity: straightforward
      modelImpact: fits
      reason: >-
        Added on the shared initializer. The factory composes one resource-free service that
        resolves the configured upgrade adapter per call, so boot never contacts the host.
      reviewedAt: 1b10a390b8a23a60509e1a118da6186c3d584846
      sources:
        - core/server/services/upgrades/index.ts
        - core/server/services/upgrades/upgrade-service.ts
        - core/server/api/endpoints/upgrades.ts
        - core/boot.js
    entryPoints:
      - upgrades/index.ts
    notes: >-
      Composition-only boundary between the Admin API and a host-owned upgrade adapter.
      Ghost validates adapter output and maps adapter rejections; it never executes updates.
    contracts:
      exports:
        summary: >-
          The TypeScript default export is a defineService root with async init and a guarded
          service getter. The implementation exports UpgradeService for direct construction.
        sources:
          - core/server/services/upgrades/index.ts
          - core/server/services/upgrades/upgrade-service.ts
        tests:
          - test/unit/server/services/upgrades/index.test.ts
          - test/unit/server/services/upgrades/upgrade-service.test.ts
      construction:
        summary: >-
          Boot awaits init in its required service Promise.all. The factory constructs one
          UpgradeService from an adapter getter and a logger, without adapter or host I/O.
        sources:
          - core/server/services/upgrades/index.ts
          - core/boot.js
        tests:
          - test/unit/server/services/upgrades/index.test.ts
      configuration:
        summary: >-
          Every call asks the adapter manager for the active adapters.upgrade adapter, so
          switching adapters applies without re-initializing. The manager caches each
          instance with its construction-time options, so option changes apply only after
          its cache is cleared.
        sources:
          - core/server/services/upgrades/index.ts
          - core/server/services/adapter-manager/index.ts
        tests:
          - test/unit/server/services/adapter-manager/upgrade-wiring.test.ts
          - test/e2e-api/admin/upgrades.test.ts
    unverified:
      - consumers
      - startup
      - lifetime
      - resources
      - failures
    blockers: []
  url:
    kind: directory
    disposition: review
    status: audited
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: complex
      modelImpact: may-change
      reason: >-
        The shared URL service must exist before routers are registered, becomes
        routing-ready later, and resets routing state in place during frontend reload.
        Decide how composition readiness relates to a usable router graph without breaking
        retained consumers or the maintenance gate. A stable facade may suffice, but folding
        both stages into one factory would create an ordering cycle.
      reviewedAt: f276513ba32b7114fd5cc4f7b02d3f82c0c31e3e
      sources:
        - core/server/services/url/index.js
        - core/server/services/url/lazy-url-service.ts
        - core/boot.js
        - core/bridge.js
      dependencies:
        complexity: straightforward
        reason: >-
          The URL implementation already accepts URL utilities, resource lookup and enumeration
          hooks. Remaining model/schema fallback, NQL/filter, matching and logging imports can be
          wired through those hooks and small helper factories. Router registration/reset remains a
          separate readiness contract; dependency wiring itself does not need frontend construction.
        preparation:
          - >-
            Make the root supply URL utilities and an enumeration closure over
            models/schema/database detection, removing implementation fallbacks.
          - >-
            Inject NQL, matching, logging/debug and utility operations into helper factories, while
            retaining one facade for serializers, RouterManager and bridge reload.
        reviewedAt: 44ad405bff4a8700af2ed51f833ebc08d25dddff
        sources:
          - core/server/services/url/index.js
          - core/server/services/url/lazy-url-service.ts
          - core/server/services/url/lazy-find-resource.ts
          - core/server/services/url/routable-resources.js
          - core/server/services/url/router-filter.ts
          - core/server/services/url/permalink-matcher.ts
          - core/frontend/services/proxy.js
          - core/bridge.js
    entryPoints:
      - url/index.js
    notes: >-
      Ownership needs review before an in-place initializer migration or relocation. Resource
      visibility, permalink rules and router registration belong together, but this root binds
      server models and has backend API consumers. Moving it wholesale into frontend routing would
      violate current dependency rules. Decide a backend-safe routing/data-access split and
      preserve backend-only availability first; no destination or source move is approved. One
      eager LazyUrlService owns ordered router registrations. Preserve readiness, real
      serializer/model composition and the different forward, reverse and enumeration contracts;
      registration/reset and same-process restart still need lifecycle coverage.
    contracts:
      exports:
        summary: >-
          CommonJS exports one mutable LazyUrlService instance; the deep module exports the
          constructor both directly and as LazyUrlService. Receiver-dependent registration,
          reset, readiness, field/relation requirements, forward URL and ownership methods are
          synchronous; resolveUrl and getRoutableResources return promises. No init/stop exists.
          Forward misses use /404/, reverse misses null, and enumeration returns a fresh array.
          Class units assert selected results and readiness, not barrel identity, unbound
          methods or the entire runtime surface.
        sources:
          - core/server/services/url/index.js
          - core/server/services/url/lazy-url-service.ts
          - core/server/services/url/routable-resources.js
        tests:
          - test/unit/server/services/url/lazy-url-service.test.ts
      construction:
        summary: >-
          Import constructs the service with createFindResource(models), retaining the models
          facade and shared URL helper. Its constructor requires a lookup function and builds
          empty router/relation/diagnostic state plus static base/exclusion filters. Lookup
          dereferences model properties per call; enumeration resolves models/schema/database
          helpers when called. Construction itself performs no resource query or registration.
          Units construct injected implementations and exercise model-hook calls with fake
          models; they do not establish eager barrel timing or shared module identity.
        sources:
          - core/server/services/url/index.js
          - core/server/services/url/lazy-url-service.ts
          - core/server/services/url/lazy-find-resource.ts
          - core/server/services/url/routable-resources.js
        tests:
          - test/unit/server/services/url/lazy-url-service.test.ts
          - test/unit/server/services/url/lazy-find-resource.test.ts
      consumers:
        summary: >-
          API serializers/search ask the retained root for fields/relations and URLs; frontend
          proxy/helpers/metadata, RouterManager and sitemap retain it too. Sitemap consumes and
          clears fresh enumeration arrays, then uses forward URLs to exclude /404/ resources.
          Service consumers include feedback, comments, imports, email, pings, machine payments,
          attribution, members, mentions, stats and webhooks. Real database integration drives
          input serializer/model/output mapper/root with manually registered primary-tag routes:
          full/narrow fields, internal/no tags, forced ID removal, relation IDs and tied tag order.
          HTTP asserts a page comment URL after uploading filtered routes and sitemap URLs after
          post/tag/author renames. Those cases do not protect every retained service/helper or
          every reverse consumer; wider wiring here is source-checked.
        sources:
          - core/server/api/endpoints/utils/serializers/input/utils/url.js
          - core/server/api/endpoints/utils/serializers/output/utils/url.js
          - core/server/api/endpoints/utils/serializers/input/comments.js
          - core/server/api/endpoints/search-index.js
          - core/server/api/endpoints/search-index-public.js
          - core/frontend/services/proxy.js
          - core/frontend/services/sitemap/site-map-manager.js
          - core/frontend/services/routing/router-manager.js
          - core/frontend/helpers/authors.js
          - core/frontend/helpers/tags.js
          - core/frontend/meta/url.js
          - core/frontend/meta/author-url.js
          - core/server/services/audience-feedback/index.ts
          - core/server/services/comments/index.ts
          - core/server/services/content-import/index.ts
          - core/server/services/email-service/email-service-wrapper.js
          - core/server/services/indexnow-ping/index.ts
          - core/server/services/machine-payments/index.js
          - core/server/services/member-attribution/index.ts
          - core/server/services/members/api.js
          - core/server/services/mentions/index.ts
          - core/server/services/slack-ping/index.ts
          - core/server/services/stats/index.ts
          - core/server/services/webhooks/index.js
        tests:
          - test/integration/url-serialization.test.js
          - test/e2e-api/members-comments/comments-lazy-url-parity.test.js
          - test/e2e-frontend/sitemap-slug-rename.test.js
      startup:
        summary: >-
          Boot imports the root before dynamic routing, passes it to RouterManager even in
          backend-only mode, and awaits route-settings start/read before synchronous router
          registration. App maintenance checks hasFinished along with its other maintenance
          gates. Each individual registration sets readiness, rather than a separate completed
          graph publication. Reverse/enumeration need models/database; forward routing needs
          registered routes and caller-supplied records. Units pin new/one-registration/reset
          readiness. VM orchestration tests assert shared URL/router dependency identity,
          awaited route-settings start and original-error propagation in both frontend modes.
          Isolated HTTP tests assert backend-only configured post/tag URLs, changed URLs after
          upload through the same Content API consumer, and frontend-only canonical rendering.
          They do not establish atomic readiness across all registrations or top-level boot
          awaiting a pending initDynamicRouting call.
        sources:
          - core/boot.js
          - core/app.js
          - core/server/services/route-settings/dynamic-routing-service.js
          - core/frontend/services/routing/router-manager.js
          - core/server/services/url/lazy-url-service.ts
        tests:
          - test/e2e-server/boot-frontend-only.isolated.test.ts
          - test/e2e-server/boot-backend-only.isolated.test.ts
          - test/unit/boot/init-dynamic-routing.test.ts
          - test/unit/server/services/url/lazy-url-service.test.ts
          - test/e2e-api/members-comments/comments-lazy-url-parity.test.js
          - test/e2e-frontend/sitemap-slug-rename.test.js
      lifetime:
        summary: >-
          Registrations append in priority order without deduplication. reset replaces the
          router array and clears readiness/derived relations/diagnostic counters, retaining
          collaborators; onRouterUpdated only clears derived relations. Reverse memoization
          stores record/null per type and lookup within one call, not across requests. Bridge
          reads settings before reset/re-registration; its units hold/reject that read and
          assert fake reset/reload sequencing, not real HTTP availability. Class tests pin
          first-router ownership, reset and same-call lookup counts. Normal boot/stop cleanup,
          retained consumers during reset and overlapping lookups/reloads are source-only.
        sources:
          - core/server/services/url/lazy-url-service.ts
          - core/bridge.js
          - core/frontend/services/routing/router-manager.js
          - core/server/ghost-server.ts
        tests:
          - test/unit/server/services/url/lazy-url-service.test.ts
          - test/unit/bridge.test.js
      configuration:
        summary: >-
          Registration captures filter/type/permalink; current registrations determine fetched
          fields/relations. Forward routing applies published post/page and public-tag gates;
          authors have no base filter. Excluded fields are stripped before router filters.
          Reverse lookup scopes publication/visibility through model queries and validates
          captured segments against that router's permalink. Enumeration applies type gates,
          including published-post joins for tags/authors, not collection filters. Units pin
          these with fake models/URL helpers; integration pins actual primary-tag composition,
          selected columns/relations and public-tag inclusion/internal-tag exclusion. Shared
          URL bases are frozen; test config replacement is not production hot reload. This
          service passes no timezone to replacePermalink, whose package default is UTC; the
          timezone bridge event only invalidates required relations, with no cited boundary test.
        sources:
          - core/server/services/url/lazy-url-service.ts
          - core/server/services/url/config.js
          - core/server/services/url/lazy-find-resource.ts
          - core/server/services/url/routable-resources.js
          - core/server/services/url/router-filter.ts
          - core/shared/url-utils.ts
          - core/bridge.js
          - package.json
        tests:
          - test/unit/server/services/url/lazy-url-service.test.ts
          - test/unit/server/services/url/lazy-find-resource.test.ts
          - test/unit/server/services/url/router-filter.test.ts
          - test/unit/server/services/url/routable-resources.test.js
          - test/integration/url-serialization.test.js
      resources:
        summary: >-
          The root owns router/filter/diagnostic memory, with no timer, subscription, network
          client or pool. Diagnostics count distinct causes without an explicit map bound.
          Borrowed model queries and enumeration have no root cancellation/drain. SQLite
          enumeration accumulates batches of 999 until an empty batch, with no cross-batch
          snapshot; other databases fetch once. Units assert fetch options and a synthetic
          result/empty SQLite pair, not real SQLite limits or concurrent writes. Real database
          integration checks selected returned records/fields. Sitemap caches and bridge
          settings subscriptions belong to consumers and are not reset/disposed by this root.
        sources:
          - core/server/services/url/lazy-url-service.ts
          - core/server/services/url/lazy-find-resource.ts
          - core/server/services/url/routable-resources.js
          - core/frontend/services/sitemap/site-map-manager.js
          - core/bridge.js
        tests:
          - test/unit/server/services/url/routable-resources.test.js
          - test/integration/url-serialization.test.js
      failures:
        summary: >-
          Thin forward records log structured errors at occurrence counts 1,10,100 and onward,
          then return /404/; unexpected URL/logger errors propagate. Units assert exact error
          details/counts and unexpected-error propagation; real model integration asserts
          missing tags degrades with a report. NQL evaluation errors warn and fail matching.
          Reverse misses and malformed path escapes return null; template construction, lookup
          and enumeration failures can reject. Matcher units pin selected malformed values and
          captures, not all invalid templates. Unknown enumeration types reject in real database
          integration. Failed bridge settings reads preserve fake routing state; faults after
          reset and query failure/recovery through real consumers lack equivalent assertions.
        sources:
          - core/server/services/url/lazy-url-service.ts
          - core/server/services/url/router-filter.ts
          - core/server/services/url/permalink-matcher.ts
          - core/server/services/url/lazy-find-resource.ts
          - core/server/services/url/routable-resources.js
          - core/bridge.js
        tests:
          - test/unit/server/services/url/lazy-url-service.test.ts
          - test/unit/server/services/url/router-filter.test.ts
          - test/unit/server/services/url/permalink-matcher.test.ts
          - test/unit/bridge.test.js
          - test/integration/url-serialization.test.js
    unverified: []
    blockers:
      - >-
        Preserve the tested frontend-only/backend-only routing and VM startup barriers. Protect
        eager root identity, complete readiness across registration, partial registration
        failure, stop and same-process restart. Normal
        boot/stop does not reset URL registrations, and bridge.init appends settings listeners;
        define ownership before replacing the root or retaining this missing cleanup.
      - >-
        Exercise retained API/frontend/service consumers across reload and replacement, with
        pending queries and failures after reset. A reverse lookup already iterating the old
        array can continue after reset; current sequencing fakes do not prove atomic routing
        publication or protect stale results and overlapping reloads.
      - >-
        Characterize cross-router canonical ownership, subdirectory and timezone boundaries
        using actual URL helpers/models. Reverse lookup rechecks only its matched router, not
        the first forward owner; forward/reverse/enumeration gates deliberately differ. The
        date units inject a UTC helper and do not protect site-timezone changes.
      - >-
        Preserve exact serializer field/relation forwarding and response cleanup for all
        consumers before migrating. Existing database and HTTP cases cover important selected
        paths, not every service, author/page shape, malformed route or model-query failure.
        Add resource-bound and concurrent-write coverage before changing enumeration/cleanup.
  users:
    kind: directory
    disposition: migrate
    status: audited
    migrated: true
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: straightforward
      modelImpact: fits
      reason: >-
        The boot-owned default root returns one ready Users instance in the services phase.
        The users and authentication endpoints read it per request instead of constructing
        their own, so both share one composition. Operation contracts are unchanged: mutable
        options, transactions and work stay per call, and the stale auth, mail and settings
        inputs are gone. No resource lifecycle or replacement model is needed.
      reviewedAt: b7cad8406041d09d9e4085c5f981f958d454fc76
      sources:
        - core/server/services/users/index.ts
        - core/server/services/users/users-service.ts
        - core/server/api/endpoints/users.js
        - core/server/api/endpoints/authentication.js
        - core/boot.js
        - test/unit/server/services/users/index.test.ts
      dependencies:
        complexity: straightforward
        reason: >-
          The root composes models, the domain event dispatcher, the PostsBulkAddTagsEvent
          constructor and the removeAuthorFromRevisions function in index.ts; the implementation
          receives them instead of requiring them. ObjectId generation stays an allowlisted
          library import. The endpoints no longer resolve the mail and settings API
          collaborators on the service's behalf.
        preparation: []
        reviewedAt: b7cad8406041d09d9e4085c5f981f958d454fc76
        sources:
          - core/server/services/users/index.ts
          - core/server/services/users/users-service.ts
          - core/server/api/endpoints/users.js
          - core/server/api/endpoints/authentication.js
          - core/server/lib/post-revisions.ts
          - core/boot.js
    entryPoints:
      - users/index.ts
    notes: >-
      One boot-owned instance replaces the two endpoint-owned ones. The prior auditedAt
      records the pre-migration baseline. The bounded migration was inspected on main
      b7cad8406041d09d9e4085c5f981f958d454fc76; the characterization units, the existing service units,
      the boot orchestration units and the reset orchestration units passed before production
      changes. Transaction behavior and the bound tagging method are preserved and pinned.
      Repeated boot, real rollback and tag-event settlement remain separate obligations below.
    contracts:
      exports:
        summary: >-
          The TypeScript root default-exports a defineService initializer with init and a
          guarded service getter typed by the UsersService class; there are no extras.
          The ready instance exposes lockAll, assignTagToUserPosts and destroyUser; only
          assignTagToUserPosts is bound. lockAll resolves an object with count, while tagging
          and destruction resolve without a result. Units pin the readiness guard, detached
          invocation of the bound method and the operation results; generic shared
          initialization and retry belong to kernel tests.
        sources:
          - core/kernel/define-service.ts
          - core/server/services/users/index.ts
          - core/server/services/users/users-service.ts
        tests:
          - test/unit/server/services/users/users-service.test.js
          - test/unit/server/services/users/index.test.ts
      construction:
        summary: >-
          Import declares the root and loads the implementation without resolving its runtime
          collaborators. The factory requires models, the domain event dispatcher, the event
          constructor and the revision author-removal function, then returns one UsersService
          synchronously. destroyUser passes the PostRevision model and its transaction to that
          function per call. The endpoints capture the root at import and read its service per request.
          Units pin the captured models reference, the per-call helper arguments and that an endpoint
          import before readiness does not evaluate the guarded getter.
        sources:
          - core/server/services/users/index.ts
          - core/server/services/users/users-service.ts
          - core/server/api/endpoints/authentication.js
          - core/server/api/endpoints/users.js
          - core/server/lib/post-revisions.ts
        tests:
          - test/unit/server/services/users/index.test.ts
      consumers:
        summary: >-
          Admin user deletion reads the ready service from the root and delegates frame options
          to destroyUser. Authentication reset passes the same ready instance to
          auth.resetAuthentication, which calls lockAll inside a transaction shared with key
          rotation and audit creation; session deletion and rescheduling happen afterward.
          assignTagToUserPosts has no external runtime caller found. Units pin both endpoints
          reading one instance with the real lockAll; HTTP tests prove deletion, owner
          reassignment, internal tagging and successful authentication reset; reset
          orchestration units inject lockAll rather than this class.
        sources:
          - core/server/api/endpoints/users.js
          - core/server/api/endpoints/authentication.js
          - core/server/auth/reset-authentication.ts
          - core/server/web/api/endpoints/admin/routes.js
          - core/server/services/users/index.ts
          - core/server/services/users/users-service.ts
        tests:
          - test/e2e-api/admin/users.test.js
          - test/legacy/api/admin/users.test.js
          - test/legacy/api/admin/authentication.test.js
          - test/unit/server/auth/reset-authentication.test.ts
          - test/unit/server/services/users/index.test.ts
      startup:
        summary: >-
          Boot requires the root in initServices and awaits its init in the shared service
          group, after models are ready and before the server accepts requests; the factory
          performs no I/O. The Admin router loads the endpoints lazily, and the update-check
          job can load the users endpoint through the API registry outside an Admin request.
          That import no longer constructs an instance or resolves the mail and settings API
          collaborators, and reading service before init throws. Authentication still resolves
          its scheduling adapter at import. Units pin the fresh endpoint load before readiness;
          HTTP tests use completed boot, not composition failure.
        sources:
          - core/boot.js
          - core/server/services/users/index.ts
          - core/server/api/endpoints/users.js
          - core/server/api/endpoints/authentication.js
          - core/server/api/endpoints/index.js
          - core/server/services/update-check/index.js
          - core/shared/express.js
          - core/server/web/parent/backend.js
          - core/server/web/api/app.js
          - core/server/web/api/endpoints/admin/app.js
          - core/server/web/api/endpoints/admin/routes.js
        tests:
          - test/e2e-api/admin/users.test.js
          - test/legacy/api/admin/authentication.test.js
          - test/unit/server/services/users/index.test.ts
      lifetime:
        summary: >-
          Successful init retains one instance for the process and repeated init returns it;
          the endpoints hold the root, not the instance. No reset, pending-operation guard or
          instance-local result cache exists. lockAll clones options when opening its own
          transaction and otherwise uses the caller's transaction. destroyUser always opens a
          new transaction and overwrites the caller's transacting field; it does not reuse an
          outer transaction. Units pin the shared endpoint instance, lockAll's cloned options
          versus the caller's options passed through unchanged, and destroyUser overwriting a
          supplied transaction in every stage; concurrency and same-process reboot stay
          unasserted.
        sources:
          - core/server/services/users/index.ts
          - core/server/services/users/users-service.ts
          - core/server/api/endpoints/users.js
          - core/server/api/endpoints/authentication.js
        tests:
          - test/unit/server/services/users/users-service.test.js
          - test/unit/server/services/users/index.test.ts
      configuration:
        summary: >-
          This root reads no configuration or settings values. Operations use current model
          results and per-call id/context/transacting options. Tagging derives an internal tag
          from the current
          author slug, reuses it by hash slug, avoids already-tagged posts and inserts new links
          with sort_order zero. Unit tests pin reuse/skip behavior and HTTP tests pin tag name
          and position; they do not cover concurrent tag creation or author changes.
        sources:
          - core/server/services/users/users-service.ts
        tests:
          - test/unit/server/services/users/users-service.test.js
          - test/legacy/api/admin/users.test.js
      resources:
        summary: >-
          The service borrows shared models/database transactions; it creates no pool, timer,
          subscription or mail transport. lockAll sequentially awaits model locks without sending
          mail. Deletion nulls revision authors, tags posts and records actions, reassigns posts,
          removes the staff API key and destroys the user within its transaction. Tagging emits
          PostsBulkAddTagsEvent before that transaction commits; dispatch does not await handlers
          and has no compensation; no current Core subscriber was found despite the collection
          update comment. Units pin the stage order inside the transaction, the per-call
          revisions helper arguments and one event dispatched before the transaction settles
          carrying only newly tagged ids; HTTP tests prove selected persisted outcomes. Full
          rollback, event settlement and shutdown drain stay unasserted.
        sources:
          - core/server/services/users/users-service.ts
          - core/server/lib/post-revisions.ts
          - core/server/models/user.js
          - core/server/models/relations/authors.js
          - core/shared/events-ts/posts-bulk-add-tags-event.ts
          - package.json
        tests:
          - test/unit/server/services/users/users-service.test.js
          - test/unit/server/lib/post-revisions.test.ts
          - test/unit/server/services/users/index.test.ts
          - test/e2e-api/admin/users.test.js
          - test/legacy/api/admin/authentication.test.js
      failures:
        summary: >-
          The factory performs no I/O, so init can only fail on a missing module and leaves
          nothing to release. Model/helper failures reject and stop later work; only ApiKey.NotFoundError is ignored
          during deletion. Managed transaction callbacks provide the database rollback boundary,
          but cannot undo an already-dispatched tag event. The users endpoint returns the async
          deletion promise inside try without awaiting it, so that catch does not remap its
          rejected promise. Authentication reset failures after commit can leave users locked
          despite a failed response. Units pin the ignored NotFoundError, other key failures
          rejecting before user deletion and the tag event staying dispatched after a later
          stage fails; success tests and fake-transaction short-circuit tests do not establish
          rollback or those partial-failure responses.
        sources:
          - core/server/services/users/users-service.ts
          - core/server/api/endpoints/users.js
          - core/server/auth/reset-authentication.ts
          - core/server/api/endpoints/authentication.js
        tests:
          - test/unit/server/auth/reset-authentication.test.ts
          - test/unit/server/services/users/index.test.ts
          - test/e2e-api/admin/users.test.js
          - test/legacy/api/admin/authentication.test.js
    unverified: []
    blockers:
      - >-
        Test repeated boot for the shared composition. Units pin one retained instance across
        both endpoints within a single module instance, not module reload or same-process
        reboot.
      - >-
        Add real transaction failure tests across lockAll and each deletion stage, including
        revision authors, tags/actions, coauthors, API keys, roles and user deletion. Current
        unit transactions are callback fakes and the reset failure test proves short-circuiting,
        not database rollback. Units pin caller transaction and options propagation against
        those fakes only.
      - >-
        Units characterize dispatch before the fake transaction settles and after a later stage
        fails, not delivery against a real commit or rollback. Source search found no current
        Core subscriber, so collection updates cannot be inferred from the producer comment.
        Record concurrent deletion/tag creation and pending-operation shutdown rather than
        inferring safety from the absence of owned timers.
      - >-
        Pin deletion error identity and HTTP status for failures inside the async service,
        optional missing API keys, and authentication reset failures after commit. Root lock
        tests simulate user.lock; model units assert save arguments, while real reset HTTP
        coverage proves a successful fixture path, not suspended-user rollback or recovery.
  verification:
    kind: directory
    disposition: review
    status: audited
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: complex
      modelImpact: fits
      reason: >-
        Review this sender with verification-trigger as one site email-verification capability.
        Its specialized delivery payload and configuration do not establish exclusive Members
        ownership. The configured collaborator can fit the chosen owner's factory without a
        defineService extension; choose that owner before a standalone migration or relocation.
      reviewedAt: 59c26cfc276946844e0694416ead8549eb637479
      sources:
        - core/server/services/verification/verification-webhook-service.ts
        - core/server/services/verification-trigger.js
        - core/server/services/members/service.js
        - core/server/services/email-service/email-service.js
        - core/server/api/endpoints/members.js
      dependencies:
        complexity: straightforward
        reason: >-
          The sender already accepts config, logging and request, but supplies global defaults and
          exports an eager singleton that Members binds into its trigger. Removing defaults and
          composing the sender in the chosen owner is a bounded dependency change; the
          signed-webhook helper is already allowlisted. This does not settle the separate question
          of whether verification remains a root or moves under Members.
        preparation:
          - >-
            Settle verification/trigger ownership before adding a root, then construct the sender
            explicitly with config, logger and request instead of its global default dependencies.
          - >-
            Preserve Members' retained sender binding and per-call configuration reads; avoid
            freezing webhook settings during construction.
        reviewedAt: 44ad405bff4a8700af2ed51f833ebc08d25dddff
        sources:
          - core/server/services/verification/verification-webhook-service.ts
          - core/server/services/verification-trigger.js
          - core/server/services/members/service.js
          - core/server/lib/signed-webhook.ts
    entryPoints:
      - verification/verification-webhook-service.ts
    notes: >-
      Review ownership together with verification-trigger. The sender implements a configured
      email-verification integration with threshold/count payloads; generic signing already
      lives in server/lib/signed-webhook. Members is its current composition point, but that
      alone does not establish private Members ownership of the site-wide email-verification
      capability. Preserve live configuration, signed delivery and delivery-before-persistence
      behavior while choosing the boundary. No source has moved.
    contracts:
      exports:
        summary: >-
          The deep TypeScript module exports VerificationWebhookService and an eager
          verificationWebhookService. There is no index, init, reset or shutdown API. The
          receiver-dependent async sendVerificationWebhook accepts amountTriggered, threshold
          and method, returning false for missing URL/type, true for a fulfilled request, or
          rejecting on failure. Payload types are compile-time constraints, not runtime
          validation. Unit tests construct injected instances; HTTP tests reach the real
          singleton but do not pin export shape or unbound invocation.
        sources:
          - core/server/services/verification/verification-webhook-service.ts
        tests:
          - test/unit/server/services/verification/verification-webhook-service.test.ts
          - test/e2e-api/admin/members.test.js
      construction:
        summary: >-
          Module import loads config, logging, signed-webhook helpers and the shared request
          module, then constructs one sender. The constructor only retains dependencies; it
          does not read feature config or send. Callers can construct the exported class with
          all three injected dependencies. Members is the only runtime importer found and
          binds the singleton method when constructing its trigger. Tests exercise injected
          construction and real consumer behavior, without a direct construction/identity test.
        sources:
          - core/server/services/verification/verification-webhook-service.ts
          - core/server/services/members/service.js
          - core/server/lib/signed-webhook.ts
        tests:
          - test/unit/server/services/verification/verification-webhook-service.test.ts
          - test/e2e-api/admin/members.test.js
      consumers:
        summary: >-
          VerificationTrigger is the sender's sole direct runtime consumer. Member-created
          events, member-import bookkeeping and newsletter preflight reach that trigger;
          methods identify admin, api, import or removed_recipients. The trigger awaits true
          before editing email_verification_required and updating the cache; false/rejection
          leaves it unmarked. Real HTTP tests assert a signed admin payload, removed-recipient
          payload/blocking and import delivery followed by suppression of another sequential
          delivery. They do not establish concurrent deduplication or every source's real
          transport behavior; trigger units use an injected webhook callback.
        sources:
          - core/server/services/members/service.js
          - core/server/services/verification-trigger.js
          - core/server/services/members/import-export/import/importer.ts
          - core/server/services/email-service/email-service.js
          - core/server/api/endpoints/members.js
        tests:
          - test/e2e-api/admin/members.test.js
          - test/legacy/api/admin/members-importer.test.js
          - test/unit/server/services/verification-trigger.test.js
      startup:
        summary: >-
          The sender is usable immediately after import and has no readiness/configuration
          guard beyond per-call URL/type checks. Boot initializes members in the required
          service batch after core settings and Stripe; members constructs its trigger after
          publishing its API and binds this sender before later asynchronous migration work.
          Standalone sender calls need no settings/database, whereas the trigger's queries and
          persistence do. HTTP tests use completed boot and do not characterize import timing,
          pre-boot direct sends or partial members initialization.
        sources:
          - core/boot.js
          - core/server/services/members/service.js
          - core/server/services/verification/verification-webhook-service.ts
        tests:
          - test/e2e-api/admin/members.test.js
          - test/legacy/api/admin/members-importer.test.js
      lifetime:
        summary: >-
          Module caching retains the singleton, and the private members trigger guard retains
          its bound sender across repeated initialization. Each send snapshots config and builds
          local body, timestamp and options; it keeps no delivery state or in-flight promise.
          There is no deduplication, cancellation or reset. Concurrent trigger calls can each
          send before either persists the required flag. Sequential suppression is tested in
          the importing consumer; singleton identity, restart and overlapping sends are not.
        sources:
          - core/server/services/verification/verification-webhook-service.ts
          - core/server/services/verification-trigger.js
          - core/server/services/members/service.js
        tests:
          - test/legacy/api/admin/members-importer.test.js
      configuration:
        summary: >-
          Each send calls the retained config accessor for webhookType, webhookUrl,
          webhookSecret and siteId. URL/type require nonempty strings, without trimming;
          non-string siteId becomes null, and absent/non-string/empty secret sends unsigned.
          The helper JSON-serializes once, signs timestamp:body using base64 HMAC-SHA256 when
          configured, and adds byte length, content type/version and timestamp headers.
          Retry-limit selection reads process.env.NODE_ENV per send. Tests configure the
          already-booted sender through test set/reset and verify signed payloads; this is
          accessor behavior, not a production reload facility. Unsigned/default/type boundaries
          and most headers are source-only.
        sources:
          - core/server/services/verification/verification-webhook-service.ts
          - core/server/lib/signed-webhook.ts
          - core/shared/config/validated.ts
          - core/shared/config/SCHEMA.md
        tests:
          - test/unit/server/services/verification/verification-webhook-service.test.ts
          - test/e2e-api/admin/members.test.js
      resources:
        summary: >-
          Each send awaits shared HTTP infrastructure with a 30-second request timeout and
          retry.limit zero in test environments or five elsewhere. The installed request/got
          defaults exclude POST from normal retries, so that option alone does not establish
          retry delivery. The request module owns a shared DNS cache/resolver and unref'd
          housekeeping timers; this root owns no dedicated transport, timer or listener.
          It exposes no abort, outstanding-request tracking or shutdown drain. Trigger event
          subscriptions and database work belong to its consumer. Existing tests intercept
          successful HTTP or inject a rejection; they do not exercise timeout/retry/drain.
        sources:
          - core/server/services/verification/verification-webhook-service.ts
          - core/server/lib/signed-webhook.ts
          - core/server/services/verification-trigger.js
          - core/server/ghost-server.ts
          - package.json
        tests: []
      failures:
        summary: >-
          Missing URL/type warns and resolves false; only missing type is unit-tested. Request
          throw/rejection is logged then rethrown, provided logging succeeds. Log prefixes use
          URL origin or an invalid-URL placeholder; error.message is appended unchanged.
          Config reads, serialization/signing and pre-request logging are outside the delivery
          catch. VerificationTrigger catches sender rejection and resolves needsVerification
          false without persisting; subsequent persistence/cache failures reject after remote
          delivery without compensation. Tests prove a logged injected request rejection and
          no-write trigger behavior, not preparation failures, post-delivery failure or exact
          error identity through the full composition.
        sources:
          - core/server/services/verification/verification-webhook-service.ts
          - core/server/lib/signed-webhook.ts
          - core/server/services/verification-trigger.js
        tests:
          - test/unit/server/services/verification/verification-webhook-service.test.ts
          - test/unit/server/services/verification-trigger.test.js
    unverified: []
    blockers:
      - >-
        Settle members ownership before relocation or a managed lifecycle. Pin the eager
        export, retained bound sender, repeated boot and direct pre-boot behavior; an audited
        entry does not establish migration readiness or authorize changing these contracts.
      - >-
        Cover missing/invalid URL and type, unsigned/non-string secrets, null/non-string siteId,
        all wire headers, exact request-error identity and preparation/logging failures. Current
        sanitization tests cover the destination prefix, not redaction of appended error text.
      - >-
        Characterize actual POST retry, timeout/redirect and pending-request shutdown behavior
        before relying on retry.limit or adding cleanup. Shared DNS/request resources need
        explicit ownership; a request timeout is not a service drain or cancellation API.
      - >-
        Test overlapping trigger sources and successful remote delivery followed by failed
        persistence/cache publication. Sequential suppression and intercepted successful
        deliveries do not prove exactly-once delivery, atomic state change or recovery.
      - >-
        Keep current source/config snapshots distinct from a future reload design. Feature
        schema validation and stricter input handling can change warn/false/reject behavior;
        introducing them during lifecycle migration needs separate compatibility evidence.
  verification-trigger.js:
    kind: file
    disposition: review
    status: audited
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: complex
      modelImpact: fits
      reason: >-
        Review this together with the verification sender. Member-list thresholds and signup
        events feed a site-wide email-verification gate used by imports, welcome email and
        newsletter delivery. Members currently constructs and retains the trigger, but that
        does not settle the capability's owner. A retained collaborator fits the chosen owner
        factory; preserve subscription cleanup and retained Email consumers when moving it.
      reviewedAt: 59c26cfc276946844e0694416ead8549eb637479
      sources:
        - core/server/services/verification-trigger.js
        - core/server/services/members/service.js
        - core/server/services/email-service/email-service-wrapper.js
        - core/server/services/email-service/email-service.js
        - core/server/api/endpoints/members.js
      dependencies:
        complexity: straightforward
        reason: >-
          Members already constructs this collaborator with live thresholds, persistence, statistics
          and a bound sender, and Email captures that same trigger. Its remaining external runtime
          imports are DomainEvents and MemberCreatedEvent, so dependency injection itself is
          bounded. Moving the trigger or deciding its lifecycle owner remains a separate ownership
          task; a new initializer should not be inferred from this classification.
        preparation:
          - >-
            Inject the member-event registration capability from the chosen owner while retaining
            the live state/threshold callbacks and single retained trigger shared with Email.
          - >-
            Coordinate its placement with the verification sender before changing imports; preserve
            the existing Members-to-Email readiness dependency.
        reviewedAt: 44ad405bff4a8700af2ed51f833ebc08d25dddff
        sources:
          - core/server/services/verification-trigger.js
          - core/server/services/members/service.js
          - core/server/services/email-service/email-service-wrapper.js
    entryPoints:
      - verification-trigger.js
    notes: >-
      Ownership remains under review with the verification sender. Member signup/import and
      removed-recipient checks drive site-wide email-verification state and sending restrictions.
      Members currently constructs the trigger and Email retains it; these construction and
      consumer relationships do not settle the final owner. Preserve thresholds, event
      subscriptions, delivery-before-persistence and caller-specific failure behavior; concurrent
      triggers and listener cleanup remain open contracts. No source has moved.
    contracts:
      exports:
        summary: >-
          CommonJS exports the VerificationTrigger constructor, not a singleton or init API.
          Instances expose async getImportThreshold, checkVerificationRequired,
          testImportThreshold and testRemovedRecipientsThreshold plus underscore methods,
          callback fields and threshold getters. Methods require their receiver except the
          constructor-bound event handler. checkVerificationRequired returns true when
          sending requires verification; getImportThreshold has no production caller found.
        sources:
          - core/server/services/verification-trigger.js
          - core/server/services/members/service.js
        tests:
          - test/unit/server/services/verification-trigger.test.js
      construction:
        summary: >-
          Members is the only runtime constructor found. After constructing its API it
          creates the trigger once, retaining that API's event repository, shared Settings,
          live configuration/cache callbacks, recipient queries and a bound webhook sender.
          The constructor synchronously subscribes to MemberCreatedEvent only if unverified
          then; it does not query counts or send a request. Tests usually stub subscription
          and construct the class directly, with two cases exercising actual dispatch.
        sources:
          - core/server/services/verification-trigger.js
          - core/server/services/members/service.js
          - core/server/services/verification/verification-webhook-service.ts
        tests:
          - test/unit/server/services/verification-trigger.test.js
      consumers:
        summary: >-
          Members exports the retained trigger and closes over it for inline/deferred imports.
          Import bookkeeping checks thresholds after row writes. Member-add HTTP calls check
          before adding and disables welcome mail when required. EmailService retains the
          trigger and checks with newsletterSend true before sending. MemberCreatedEvent
          reaches the bound API/admin handler after member signup-event persistence; transactional
          dispatch waits for commit. HTTP tests cover real member creation, intercepted signed
          webhook requests and newsletter blocking;
          importer failure tests instead inject a throwing trigger.
        sources:
          - core/server/services/members/service.js
          - core/server/services/members/import-export/import/importer.ts
          - core/server/api/endpoints/members.js
          - core/server/services/email-service/email-service-wrapper.js
          - core/server/services/email-service/email-service.js
          - core/server/services/members/members-api/repositories/member-repository.js
          - core/server/services/verification-trigger.js
        tests:
          - test/e2e-api/admin/members.test.js
          - test/integration/services/email-service/batch-sending.test.js
          - test/unit/server/services/email-service/email-service.test.js
          - test/unit/server/services/members/import-export/import/error-handling.test.ts
      startup:
        summary: >-
          Boot invokes members.init before emailService.init in the required service batch.
          Members synchronously publishes the trigger before its later Stripe migration await,
          so email construction can capture it. No trigger readiness guard exists; callbacks
          assume settings, models and the members event repository are usable. Later members
          initialization failure can leave the published trigger and subscription in place.
          Existing HTTP coverage uses complete boot, not this partial-initialization boundary.
        sources:
          - core/boot.js
          - core/server/services/members/service.js
          - core/server/services/email-service/email-service-wrapper.js
          - core/server/services/verification-trigger.js
        tests:
          - test/e2e-api/admin/members.test.js
      lifetime:
        summary: >-
          The private members variable retains one trigger across repeated init; its exported
          property can be replaced without changing the import closures or EmailService's
          retained reference. No reset/unsubscribe exists. Initial verified state determines
          subscription presence permanently for that instance. Later verification/config
          changes affect callbacks but do not add or remove the listener. The required-setting
          check suppresses already completed triggers; it is not a pending-request guard,
          so overlapping events/checks can send before either writes the setting.
        sources:
          - core/server/services/members/service.js
          - core/server/services/verification-trigger.js
          - core/server/services/email-service/email-service.js
          - core/server/ghost-server.ts
        tests: []
      configuration:
        summary: >-
          Thresholds and verified/required state are read live through callbacks. Only finite
          numeric thresholds enable checks; comparisons trigger strictly above the boundary.
          API/admin use recent source totals against max(configured threshold, all-time
          source-member signup-event total),
          with a pagination-based current-event adjustment. Imports compare recent imports
          against max(configured threshold, that signup-event total minus recent imports); the separate
          getImportThreshold helper does not subtract imports. These event totals are not a
          count of current members. Removed-recipient checks run
          only for newsletter sends, use a cheap recent-volume bound, then count distinct
          addresses absent from members up to threshold plus one. Recent query windows use 30
          days. Unit tests pin threshold arithmetic and query arguments against fake totals;
          HTTP tests pin selected triggering outcomes, not the real signup
          query meaning, every date boundary or all removed-recipient query semantics.
        sources:
          - core/server/services/verification-trigger.js
          - core/server/services/members/service.js
          - core/server/services/members/members-api/repositories/event-repository.js
        tests:
          - test/unit/server/services/verification-trigger.test.js
          - test/e2e-api/admin/members.test.js
      resources:
        summary: >-
          An unverified instance owns a bound listener retained by shared DomainEvents. Queries
          borrow shared database/model/repository owners; the webhook sender uses shared HTTP
          infrastructure with a 30-second request timeout and explicit retry-limit options.
          The trigger creates no timer or dedicated connection and retains no cancellation,
          pending-work queue or shutdown hook. DomainEvents catches/logs handler rejections;
          allSettled tracks test delivery only and is not a production drain. Dispatch units
          use fake repositories and HTTP tests intercept delivery; neither establishes
          timeout/retry behavior or resource cleanup.
        sources:
          - core/server/services/verification-trigger.js
          - core/server/services/members/service.js
          - core/server/services/verification/verification-webhook-service.ts
          - core/server/lib/signed-webhook.ts
          - core/server/ghost-server.ts
          - package.json
        tests:
          - test/unit/server/services/verification-trigger.test.js
          - test/e2e-api/admin/members.test.js
      failures:
        summary: >-
          False or rejected webhook delivery resolves needsVerification false without writing
          the setting. Successful delivery is followed by awaited Settings.edit, then an
          explicit synchronous cache update; persistence/cache errors reject after delivery
          without compensation. Optional throwOnTrigger produces EMAIL_VERIFICATION_NEEDED
          after state change, but current runtime callers request no throw. Count/callback
          errors reject direct callers; event delivery logs them. Import bookkeeping reports
          failures while keeping committed rows/results, whereas HTTP/email checks propagate.
          Unit tests prove payloads, no-write delivery failures and exact setting/cache arguments,
          not persistence failures, concurrency or atomic state publication.
        sources:
          - core/server/services/verification-trigger.js
          - core/server/services/members/import-export/import/importer.ts
          - core/server/api/endpoints/members.js
          - core/server/services/email-service/email-service.js
        tests:
          - test/unit/server/services/verification-trigger.test.js
          - test/unit/server/services/members/import-export/import/error-handling.test.ts
          - test/e2e-api/admin/members.test.js
    unverified: []
    blockers:
      - >-
        Pin retained trigger identity, members-to-email boot ordering, construction failure
        and repeated boot/cleanup. Test initially verified then unverified and the reverse:
        subscription presence is captured at construction while the decisions read live state.
      - >-
        Characterize simultaneous events, import completion and newsletter checks through
        webhook delivery and setting/cache publication. No shared promise or atomic guard
        establishes single delivery. Existing HTTP cases settle events between operations.
      - >-
        Restore precise first-crossing coverage. The member HTTP test adds another member
        past the threshold and accepts an inexact reported count. Its sibling-writer race
        explanation predates the repository awaiting signup-event persistence before dispatch;
        it is not evidence of that race today. Pin transactional publication and pagination
        behavior with current production composition, rather than preserving stale commentary.
      - >-
        Cover successful delivery followed by Settings.edit or cache-update failure, changed
        verification state while delivery is pending, malformed callbacks and retry behavior.
        Assert direct request, event-handler and import-bookkeeping outcomes separately;
        source-only failure channels are not all exercised by the existing delivery tests.
      - >-
        Establish event-listener and in-flight request/query ownership before relocation or
        lifecycle disposal. A raw DomainEvents listener cannot be removed through a trigger
        API today; test-only allSettled and a request timeout do not establish shutdown drain.
  webhooks:
    kind: directory
    disposition: migrate
    status: audited
    auditedAt: bf6a3ea2e32b7b551980c02c745e3c9b74e3ea0e
    migration:
      complexity: complex
      modelImpact: fits
      reason: >-
        Unify the dispatch graph and separately constructed management API under a clear
        root. Listener registration currently keeps existing named callbacks, so rebuilding
        the trigger graph does not replace what those callbacks capture; preserve
        intentional identity and remove only newly acquired registrations after a failed
        start. One returned composition can fit, but consumer and listener ownership need
        coordinated changes.
      reviewedAt: f276513ba32b7114fd5cc4f7b02d3f82c0c31e3e
      sources:
        - core/server/services/webhooks/index.js
        - core/server/services/webhooks/listen.js
        - core/server/services/webhooks/webhooks-service.js
        - core/server/api/endpoints/webhooks.js
        - core/boot.js
      dependencies:
        complexity: straightforward
        reason: >-
          The root already composes the serializer, payload builder and trigger. Remaining
          dependencies are a deferred API serializer lookup, member-metafield filtering and trigger
          transport/logging/config helpers. They fit those existing seams if API and Members access
          remains deferred; listener lifetime and duplicate registration are separate migration
          concerns.
        preparation:
          - >-
            Inject serializer operations and member-metafield filtering from index.ts, preserving
            deferred API/Members access rather than loading the API graph during root construction.
          - >-
            Select the transport in the root using the current internal-IP policy, and inject
            debug/logger/version plus collection helpers without moving crypto out of its direct
            allowance.
        reviewedAt: 44ad405bff4a8700af2ed51f833ebc08d25dddff
        sources:
          - core/server/services/webhooks/index.js
          - core/server/services/webhooks/serialize.js
          - core/server/services/webhooks/webhook-trigger.js
          - core/server/services/webhooks/listen.js
          - core/boot.js
    entryPoints:
      - webhooks/index.js
      - webhooks/webhooks-service.js
    notes: >-
      Boot owns event dispatch; a separate deep factory supplies the Admin management endpoint.
      Preserve both compositions and distinguish transaction-deferred events from durable delivery.
    contracts:
      exports:
        summary: >-
          CommonJS root exports synchronous, receiver-independent listen, returning undefined.
          It exposes no trigger instance, init or disposer. The deep webhooks-service factory
          returns a fresh receiver-dependent WebhooksService with async add; the trigger class
          and serialize/payload/listener factories are private support. The endpoint retains
          one management instance and uses its add, while edit/destroy call the model directly.
          HTTP tests cover selected management results. Root units pin the exact export, listen
          only and returning undefined, with nothing registered before it; endpoint units pin add
          through the management service, which reads the shared Webhook model per call, duplicate
          rejection, the three foreign-key translations and rethrow of other failures.
        sources:
          - core/server/services/webhooks/index.js
          - core/server/services/webhooks/webhooks-service.js
          - core/server/services/webhooks/webhook-trigger.js
          - core/server/api/endpoints/webhooks.js
        tests:
          - test/unit/server/services/webhooks/index.test.ts
          - test/unit/server/api/endpoints/webhooks.test.ts
          - test/unit/server/services/webhooks/webhook-service.test.js
          - test/e2e-api/admin/webhooks.test.js
      construction:
        summary: >-
          Every listen call requires models/limits/events/URL/member dependencies, creates a
          serializer and payload closure plus WebhookTrigger, then registers event callbacks.
          Transport is selected once per trigger unless injected. Member metafields use a
          callback reading membersService.api when invoked, rather than a captured member API.
          The management endpoint constructs its own service at module evaluation, normally
          reached during lazy Admin router setup. No other production constructor/deep consumer
          was found. Units test injected trigger and serializer construction, including transport
          choice. Root units compose the real root: transport follows the config set before listen
          (user agent on the wire), and URL relations and the members API are read when an event
          is serialized. Whether a repeated listen's new trigger reaches any listener is not
          observable through the root, since its listeners are not replaced (see lifetime).
        sources:
          - core/server/services/webhooks/index.js
          - core/server/services/webhooks/webhook-trigger.js
          - core/server/services/webhooks/serialize.js
          - core/server/api/endpoints/webhooks.js
          - core/server/api/endpoints/index.js
          - core/server/web/api/app.js
          - core/server/web/api/endpoints/admin/app.js
          - core/shared/express.js
        tests:
          - test/unit/server/services/webhooks/index.test.ts
          - test/unit/server/api/endpoints/webhooks.test.ts
          - test/unit/server/services/webhooks/trigger.test.js
          - test/unit/server/services/webhooks/serialize.test.js
      consumers:
        summary: >-
          Boot calls listen. Common model events drive post/page/tag/member delivery; response
          finish with full cache invalidation emits site.changed. Listeners skip importing and
          launch trigger without awaiting it. The Admin endpoint separately adds definitions
          and authorizes management. Root units pin the 29-event mapping, one named listener per
          event, lookups for the emitted event and import suppression. HTTP suites exercise selected publish/add/edit payloads
          through real API/model composition and a Nock-backed receiver; custom-field tests
          assert current/previous values and omissions. Import suppression is checked after
          importer settlement plus a bounded wait, not a webhook drain. These tests establish
          intercepted payloads, not durable delivery, every event or external receiver behavior.
          A focused local run completed custom-field payload assertions but reproducibly timed
          out draining DomainEvents after the create-with-fields case; settlement remains a gap.
        sources:
          - core/boot.js
          - core/server/services/webhooks/listen.js
          - core/server/models/post.js
          - core/server/models/tag.js
          - core/server/models/member.js
          - core/server/web/parent/middleware/emit-events.js
          - core/server/api/endpoints/webhooks.js
        tests:
          - test/e2e-webhooks/posts.test.js
          - test/e2e-webhooks/pages.test.js
          - test/e2e-webhooks/tags.test.js
          - test/e2e-webhooks/members.test.js
          - test/e2e-webhooks/site.test.js
          - test/e2e-webhooks/zapier-custom-fields.test.ts
          - test/e2e-webhooks/posts-importer.test.ts
          - test/e2e-api/admin/webhooks.test.js
          - test/unit/server/services/webhooks/index.test.ts
      startup:
        summary: >-
          Importing the root defers model/service construction until listen. Boot prepares
          models, limits and dynamic routing before invoking listen alongside service init;
          member access is deferred until serialization. Listen registers synchronously and
          does not wait for any delivery. Management construction has no boot hook and can
          occur on the first Admin request. Existing HTTP suites exercise a normal booted
          pipeline. Boot units pin one listen after Mentions init, boot awaiting what it returns
          before jobs and a failure stopping jobs. Explicit settings/member/URL readiness failures
          and rollback after a later initializer fails remain untested.
        sources:
          - core/boot.js
          - core/server/services/webhooks/index.js
          - core/server/services/webhooks/listen.js
          - core/server/web/parent/backend.js
          - core/server/web/api/app.js
          - core/server/web/api/endpoints/admin/routes.js
        tests:
          - test/unit/boot/init-services.test.ts
          - test/unit/server/services/webhooks/index.test.ts
          - test/e2e-webhooks/members.test.js
          - test/e2e-api/admin/webhooks.test.js
      lifetime:
        summary: >-
          Registration skips any event already having a callback named processWebhookTrigger;
          repeated listen therefore creates new collaborators but retains previous callbacks
          and triggers for registered events. No unsubscribe/reset exists. Each trigger queries
          definitions afresh, builds one payload, and awaits sends serially; separate events
          can overlap without a shared queue. Models are retained by reference across async
          serialization. Model transactions defer event emission until committed=true, using
          mutable model state, not a persisted delivery record. Units assert one payload for
          two hooks and serial delivery: the next request starts once the previous settles. Root
          units pin that a repeated listen adds no listener. Members HTTP tests capture an emitted model then
          call the serializer directly; they do not prove rollback-safe network delivery.
        sources:
          - core/server/services/webhooks/index.js
          - core/server/services/webhooks/listen.js
          - core/server/services/webhooks/webhook-trigger.js
          - core/server/services/webhooks/serialize.js
          - core/server/lib/common/events.js
          - core/server/models/base/plugins/events.js
          - core/server/models/base/bookshelf.js
        tests:
          - test/unit/server/services/webhooks/index.test.ts
          - test/unit/server/services/webhooks/trigger.test.js
          - test/e2e-api/members/webhooks.test.js
      configuration:
        summary: >-
          The trigger captures transport according to security:allowWebhookInternalIPs:
          default false uses request-external; true uses @tryghost/request. Existing listeners
          retain that choice. Limits are consulted per event; URL relations and the current
          members API are read during serialization. Every delivery reads stored target/secret,
          sends current Ghost Content-Version rather than stored api_version, and optionally
          signs JSON body plus timestamp with HMAC-SHA256. Request options ask for two seconds
          and retry limit five outside tests, zero in tests; installed Got excludes POST from
          retry methods, so the limit alone does not enable retries. Units assert transport,
          exact request options and retry limits, body/headers and exact signature; root units
          assert each transport's user agent and the safe-version Content-Version on the wire;
          the integration suite shows @tryghost/request abandoning a target slower than two
          seconds and not retrying a POST outside tests. request-external's test-only init hook
          replaces the timeout, so its production timeout is source-only.
          Test config replacement does not establish hot reload of captured collaborators.
        sources:
          - core/server/services/webhooks/index.js
          - core/server/services/webhooks/webhook-trigger.js
          - core/server/services/webhooks/serialize.js
          - core/server/services/limits.ts
          - core/server/lib/request-external.js
          - core/shared/config/defaults.json
        tests:
          - test/unit/server/services/webhooks/trigger.test.js
          - test/unit/server/services/webhooks/serialize.test.js
          - test/unit/server/services/webhooks/index.test.ts
          - test/integration/services/webhook-request.test.ts
          - test/e2e-webhooks/site.test.js
      resources:
        summary: >-
          The root retains common EventEmitter callbacks and borrows shared models/services
          and HTTP transports. There is no jobs-service queue, delivery outbox, replay worker,
          in-flight registry, stop hook or drain. Transaction event deferral is memory-only.
          request-external owns shared keep-alive agents; @tryghost/request owns a shared DNS
          cache with unref'd cleanup timers. Neither is disposed by this root. Trigger awaits
          each request and a 410 destroy, but last-triggered edits are launched without awaiting
          persistence. Nock transport tests use fake models/payload/limits and prove POST routing
          and outcomes for both clients, not socket cleanup or database completion; units pin that
          the trigger does not wait for the last-triggered write.
        sources:
          - core/server/services/webhooks/index.js
          - core/server/services/webhooks/listen.js
          - core/server/services/webhooks/webhook-trigger.js
          - core/server/lib/common/events.js
          - core/server/lib/request-external.js
          - core/server/models/base/plugins/events.js
          - core/server/data/schema/schema.js
        tests:
          - test/integration/services/webhook-request.test.ts
      failures:
        summary: >-
          Lookup/payload failures log and return without sends. Ordinary asynchronous request
          failures call onError and allow the loop to continue; JSON/HMAC/model-getter, synchronous
          request or callback/logger exceptions outside preparation can reject an unobserved
          trigger. Update/destroy rejection only warns. Source reveals a transport mismatch:
          default request-external retains HTTP status on error.response.statusCode, while
          onError reads error.statusCode for 410 removal and status persistence; the alternate
          request wrapper flattens response fields. The integration suite reproduces it with real
          Got errors and pins today's behavior: through request-external a 410 records an
          undefined status instead of destroying the webhook, and other error statuses record
          as undefined; through @tryghost/request a 410 destroys and a 500 records 500. Units pin
          awaited 410 removal, write and removal failure warnings, failure continuation and
          lookup failure logging.
          Management rejects duplicates and translates selected foreign-key errors; check/add
          has no atomic uniqueness guard. HTTP tests assert duplicate/orphan 422 and selected
          management statuses; units assert payload failure logging and injected error text,
          and endpoint units assert duplicate and foreign-key translation.
        sources:
          - core/server/services/webhooks/webhook-trigger.js
          - core/server/services/webhooks/webhooks-service.js
          - core/server/models/webhook.js
          - core/server/lib/request-external.js
          - core/server/data/schema/schema.js
          - core/server/api/endpoints/webhooks.js
        tests:
          - test/unit/server/services/webhooks/trigger.test.js
          - test/unit/server/services/webhooks/webhook-service.test.js
          - test/integration/services/webhook-request.test.ts
          - test/unit/server/api/endpoints/webhooks.test.ts
          - test/e2e-api/admin/webhooks.test.js
    unverified: []
    blockers:
      - >-
        Partial registration after a failure part-way through listen is unpinned. Decide how
        retained listener dependencies and captured transport survive restart without duplicates
        or leaked listeners.
      - >-
        Add real commit/rollback-to-delivery coverage and shutdown assertions. Preserve import suppression and current/previous payloads
        without assuming mutable event models are snapshots or intercepted sends are durable.
        On committed=false the model retains ghostEvents; reuse would skip registering another
        transaction callback. No production reuse path or reproduction has been established.
      - >-
        Resolve default-transport error status handling: request-external errors carry the status
        on error.response, so a 410 is not deleted and the recorded status is undefined. Real Got
        errors reproduce it; the tests pin today's behavior.
      - >-
        Cover synchronous/post-preparation exceptions and concurrent duplicate creation. Define delivery/replay and disposal ownership
        before adding a lifecycle; existing code has no durable queue or drain.
