apiVersion: specs.gwptd.io/v1alpha1
kind: KernelBuildInputManifest
metadata:
  name: kernel-build-all-inputs
  lifecycle: active
  activeEndpointCount: 35

# These policies are executable vocabulary, not documentation labels. The
# loader requires the exact bounded set and includes it in policyDigest.
sourceModePolicies:
  active_product_spec:
    countedActiveEndpoint: true
    buildEligible: true
    completeness: successful_complete_collection_run
  excluded_product_spec:
    countedActiveEndpoint: false
    buildEligible: false
    completeness: excluded_from_kernel_build
  external_reference:
    countedActiveEndpoint: false
    buildEligible: true
    completeness: producer_guarded_reference_snapshot
  kernel_static_reference:
    countedActiveEndpoint: false
    buildEligible: true
    completeness: declared_relation_only_no_content_digest
  prepared_external:
    countedActiveEndpoint: false
    buildEligible: true
    completeness: validated_before_business_dml
  legacy_recovery:
    countedActiveEndpoint: false
    buildEligible: true
    completeness: recovered_historical_payload_not_live_collection
  acknowledged_loss_ledger:
    countedActiveEndpoint: false
    buildEligible: true
    completeness: named_finite_allowlist_with_file_digest
  formula_contract:
    countedActiveEndpoint: false
    buildEligible: true
    completeness: file_digest_and_schema_gate
  metric_spec:
    countedActiveEndpoint: false
    buildEligible: true
    completeness: file_digest_and_golden_gate

mutationModePolicies:
  upsert:
    writes: true
    destructive: false
    strategy: insert_or_update
  upsert_and_quarantine_reconcile:
    writes: true
    destructive: false
    strategy: identity_upsert_and_status_update
  snapshot_reconcile:
    writes: true
    destructive: true
    strategy: authoritative_scope_replace_or_retract
  scope_replace_and_upsert:
    writes: true
    destructive: true
    strategy: delete_bounded_scope_then_upsert
  dedupe_delete:
    writes: true
    destructive: true
    strategy: deterministic_duplicate_retraction
  derived_snapshot_replace:
    writes: true
    destructive: true
    strategy: replace_calculation_scope
  validation_only:
    writes: false
    destructive: false
    strategy: fail_closed_read_gate

sources:
  - id: product:1c.supplier_catalog
    mode: active_product_spec
    inputMode: full_snapshot
    selection: latest_attempt_exact_run_no_fallback
    contract: {freshnessHours: 27, windowDays: null, endOffsetDays: 0, requiredScopes: []}
    endpoint: 1c.supplier_catalog
    tables: [onec_supplier_catalog]
  - id: product:wb.cards_list
    mode: active_product_spec
    inputMode: full_snapshot
    selection: latest_attempt_exact_run_no_fallback
    contract: {freshnessHours: 27, windowDays: null, endOffsetDays: 0, requiredScopes: []}
    endpoint: wb.cards_list
    tables: [wb_cards_list]
  - id: product:wb.orders
    mode: active_product_spec
    inputMode: rolling_journal
    selection: latest_attempt_exact_run_covers_replay_window_no_fallback
    contract: {freshnessHours: 27, windowDays: 7, endOffsetDays: 0, requiredScopes: []}
    endpoint: wb.orders
    tables: [wb_orders]
  - id: product:wb.sales
    mode: active_product_spec
    inputMode: rolling_journal
    selection: latest_attempt_exact_run_covers_replay_window_no_fallback
    contract: {freshnessHours: 27, windowDays: 7, endOffsetDays: 0, requiredScopes: []}
    endpoint: wb.sales
    tables: [wb_sales]
  - id: product:wb.orders_fbs
    mode: active_product_spec
    inputMode: rolling_journal
    selection: latest_attempt_exact_run_covers_replay_window_no_fallback
    contract: {freshnessHours: 27, windowDays: 7, endOffsetDays: 0, requiredScopes: []}
    endpoint: wb.orders_fbs
    tables: [wb_orders_fbs]
  - id: product:ozon.product_info_list
    mode: active_product_spec
    inputMode: full_snapshot
    selection: latest_attempt_exact_run_no_fallback
    contract: {freshnessHours: 27, windowDays: null, endOffsetDays: 0, requiredScopes: []}
    endpoint: ozon.product_info_list
    tables: [ozon_product_info_list]
  - id: product:ozon.product_sku_map
    mode: active_product_spec
    inputMode: full_snapshot
    selection: latest_attempt_exact_run_no_fallback
    contract: {freshnessHours: 27, windowDays: null, endOffsetDays: 0, requiredScopes: []}
    endpoint: ozon.product_sku_map
    tables: [ozon_product_sku_map]
  - id: product:ym.offer_mappings
    mode: active_product_spec
    inputMode: full_snapshot
    selection: latest_attempt_exact_run_no_fallback
    contract: {freshnessHours: 27, windowDays: null, endOffsetDays: 0, requiredScopes: []}
    endpoint: ym.offer_mappings
    tables: [ym_offer_mappings]
    # Восемь дочерних таблиц развёрнуты из массивов offer_mappings (волна C).
    # Ядро их пока не читает.
    intake_only: [ym_offer_mappings_offer_campaigns, ym_offer_mappings_offer_commoditycodes, ym_offer_mappings_offer_manuals, ym_offer_mappings_offer_mediafiles_manuals, ym_offer_mappings_offer_mediafiles_pictures, ym_offer_mappings_offer_mediafiles_videos, ym_offer_mappings_offer_params, ym_offer_mappings_showcaseurls]
  - id: product:lamoda.nomenclatures
    mode: active_product_spec
    inputMode: full_snapshot
    selection: latest_attempt_exact_run_no_fallback
    contract: {freshnessHours: 27, windowDays: null, endOffsetDays: 0, requiredScopes: []}
    endpoint: lamoda.nomenclatures
    tables: [lamoda_nomenclatures]
    # Дочерние таблицы из массивов default и pictures (волна C). Ядро не читает.
    intake_only: [lamoda_nomenclatures_default, lamoda_nomenclatures_img320x461]
  - id: product:wb.warehouse_stocks
    mode: active_product_spec
    inputMode: full_snapshot
    selection: latest_attempt_exact_run_no_fallback
    contract: {freshnessHours: 27, windowDays: null, endOffsetDays: 0, requiredScopes: []}
    endpoint: wb.warehouse_stocks
    tables: [wb_warehouse_stocks]
  - id: product:wb.stocks_fbs
    mode: active_product_spec
    inputMode: full_snapshot
    selection: latest_attempt_exact_run_no_fallback
    contract: {freshnessHours: 27, windowDays: null, endOffsetDays: 0, requiredScopes: []}
    endpoint: wb.stocks_fbs
    tables: [wb_stocks_fbs]
  - id: product:ozon.stocks
    mode: active_product_spec
    inputMode: full_snapshot
    selection: latest_attempt_exact_run_no_fallback
    contract: {freshnessHours: 27, windowDays: null, endOffsetDays: 0, requiredScopes: []}
    endpoint: ozon.stocks
    tables: [ozon_stocks]
    # Дочерняя таблица из массива stocks (волна C). Ядро пока не читает.
    intake_only: [ozon_stocks_stocks]
  - id: product:ozon.stock_on_warehouses
    mode: active_product_spec
    inputMode: full_snapshot
    selection: latest_attempt_exact_run_no_fallback
    contract: {freshnessHours: 27, windowDays: null, endOffsetDays: 0, requiredScopes: []}
    endpoint: ozon.stock_on_warehouses
    tables: [ozon_stock_on_warehouses]
  - id: product:ym.stocks
    mode: active_product_spec
    inputMode: full_snapshot
    selection: latest_attempt_exact_run_no_fallback
    contract:
      freshnessHours: 27
      windowDays: null
      endOffsetDays: 0
      requiredScopes: [{key: fbs, mpId: 6}, {key: fby, mpId: 8}]
    endpoint: ym.stocks
    tables: [ym_stocks]
    # Дочерняя таблица из массива stocks (волна C). Ядро пока не читает.
    intake_only: [ym_stocks_stocks]
  - id: product:lamoda.inventory
    mode: active_product_spec
    inputMode: full_snapshot
    selection: latest_attempt_exact_run_no_fallback
    contract: {freshnessHours: 27, windowDays: null, endOffsetDays: 0, requiredScopes: []}
    endpoint: lamoda.inventory
    tables: [lamoda_inventory]
  - id: product:wb.prices
    mode: active_product_spec
    inputMode: full_snapshot
    selection: latest_attempt_exact_run_no_fallback
    contract: {freshnessHours: 27, windowDays: null, endOffsetDays: 0, requiredScopes: []}
    endpoint: wb.prices
    tables: [wb_prices]
    # Дочерняя таблица размеров цены (волна C). Ядро пока не читает.
    intake_only: [wb_prices_sizes]
  - id: product:ozon.prices
    mode: active_product_spec
    inputMode: full_snapshot
    selection: latest_attempt_exact_run_no_fallback
    contract: {freshnessHours: 27, windowDays: null, endOffsetDays: 0, requiredScopes: []}
    endpoint: ozon.prices
    tables: [ozon_prices]
  - id: product:ym.prices
    mode: active_product_spec
    inputMode: full_snapshot
    selection: latest_attempt_exact_run_no_fallback
    contract: {freshnessHours: 27, windowDays: null, endOffsetDays: 0, requiredScopes: []}
    endpoint: ym.prices
    tables: [ym_prices]
  - id: product:wb.paid_storage
    mode: active_product_spec
    inputMode: rolling_journal
    selection: latest_attempt_exact_run_covers_replay_window_no_fallback
    contract: {freshnessHours: 27, windowDays: 7, endOffsetDays: 0, requiredScopes: []}
    endpoint: wb.paid_storage
    tables: [wb_paid_storage]
  - id: product:ym.storage
    mode: active_product_spec
    inputMode: rolling_journal
    selection: latest_attempt_exact_run_covers_replay_window_no_fallback
    contract: {freshnessHours: 27, windowDays: 30, endOffsetDays: 1, requiredScopes: []}
    endpoint: ym.storage
    tables: [ym_storage]
  - id: product:wb.finance_reports
    mode: active_product_spec
    inputMode: rolling_journal
    selection: latest_attempt_exact_run_covers_replay_window_no_fallback
    contract: {freshnessHours: 27, windowDays: 14, endOffsetDays: 0, requiredScopes: []}
    endpoint: wb.finance_reports
    tables: [wb_finance_reports]
  - id: product:ozon.finance_transactions
    mode: active_product_spec
    inputMode: rolling_journal
    selection: latest_attempt_exact_run_covers_replay_window_no_fallback
    contract: {freshnessHours: 27, windowDays: 14, endOffsetDays: 0, requiredScopes: []}
    endpoint: ozon.finance_transactions
    tables: [ozon_finance_transactions]
    # Дочерние таблицы из $.items[] и $.services[] (волна C). Ядро пока не читает.
    intake_only: [ozon_finance_transactions_items, ozon_finance_transactions_services]
  # This source is always attested with the kernel build so an explicit YM
  # finance projection can use only one sealed report. build_fact_finance
  # leaves it untouched unless GWPTD_ENABLE_YM_FINANCE_PROJECTION=1.
  - id: product:ym.finance
    mode: active_product_spec
    inputMode: rolling_journal
    selection: latest_attempt_exact_run_covers_replay_window_no_fallback
    contract: {freshnessHours: 27, windowDays: 30, endOffsetDays: 1, requiredScopes: []}
    endpoint: ym.finance
    tables: [ym_finance]
  - id: product:ozon.postings_fbo
    mode: active_product_spec
    inputMode: rolling_journal
    selection: latest_attempt_exact_run_covers_replay_window_no_fallback
    contract: {freshnessHours: 27, windowDays: 7, endOffsetDays: -1, requiredScopes: []}
    endpoint: ozon.postings_fbo
    tables: [ozon_postings_fbo]
    # Дочерняя таблица из $.products[] (волна C). Ядро пока не читает.
    # +1 (12.08.2026): финансы позиций переехали в свою таблицу. Оба массива
    # писались в ozon_postings_fbo_products и давали ДВЕ половинчатые строки на
    # один товар — 353 каталожных против 308 финансовых. У соседа FBS они
    # разведены с самого начала.
    intake_only: [ozon_postings_fbo_products, ozon_postings_fbo_financial_data_products]
  - id: product:ozon.postings_fbs
    mode: active_product_spec
    inputMode: rolling_journal
    selection: latest_attempt_exact_run_covers_replay_window_no_fallback
    contract: {freshnessHours: 27, windowDays: 7, endOffsetDays: -1, requiredScopes: []}
    endpoint: ozon.postings_fbs
    tables: [ozon_postings_fbs]
    # Дочерние таблицы развёрнуты из блобов $.products[] и
    # $.financial_data.products[] волной C. Ядро их пока не читает — они помечены
    # intake_only, чтобы контракт покрытия tables ∪ intake_only сходился с
    # ProductSpec, но замыкание ETL их не требовало.
    intake_only: [ozon_postings_fbs_products, ozon_postings_fbs_financial_data_products]
  # A separate 30-day status-change journal closes orders whose creation date
  # is outside the base seven-day FBS window.  It shares the typed relation,
  # but must have its own sealed approved payload set.
  - id: product:ozon.postings_fbs_changed
    mode: active_product_spec
    inputMode: rolling_journal
    selection: latest_attempt_exact_run_covers_replay_window_no_fallback
    contract: {freshnessHours: 27, windowDays: 30, endOffsetDays: -1, requiredScopes: []}
    endpoint: ozon.postings_fbs_changed
    tables: [ozon_postings_fbs]
    # 12.08.2026: добор приведён к основному сбору один в один. До этого он
    # объявлял 16 полей из 58 и ни одного потомка, при том что ответ площадки
    # у обоих одинаков. В таблице было 5456 отправлений и полные данные лишь у
    # 21 — остальные видел только добор.
    intake_only: [ozon_postings_fbs_products, ozon_postings_fbs_financial_data_products]
  - id: product:ym.stats_orders
    mode: active_product_spec
    inputMode: rolling_journal
    selection: latest_attempt_exact_run_covers_replay_window_no_fallback
    contract:
      freshnessHours: 27
      windowDays: 7
      endOffsetDays: 0
      requiredScopes: [{key: fbs, mpId: 6}, {key: fby, mpId: 8}]
    endpoint: ym.stats_orders
    tables: [ym_stats_orders]
    # Дочерние таблицы развёрнуты из массива items_json (волна C). Ядро пока не читает.
    # +3 (12.08.2026): денежные потомки — commissions, payments, subsidies.
    # Существовали с волны DDL 09.08 и стояли пустыми: площадка присылает
    # массивы, а цель записи объявлена не была. Ядро их тоже пока не читает,
    # но перечень обязан совпадать с описанием — иначе сборка отказывается
    # строить с «ProductSpec/table mapping differs».
    intake_only: [ym_stats_orders_items, ym_stats_orders_items_details, ym_stats_orders_items_prices,
                  ym_stats_orders_commissions, ym_stats_orders_payments, ym_stats_orders_subsidies]
  # The status-change journal is a distinct complete input even though the
  # parser writes into the same typed relation as the creation-date journal.
  - id: product:ym.stats_orders_changed
    mode: active_product_spec
    inputMode: rolling_journal
    selection: latest_attempt_exact_run_covers_replay_window_no_fallback
    contract:
      freshnessHours: 27
      windowDays: 30
      endOffsetDays: 0
      requiredScopes: [{key: fbs, mpId: 6}, {key: fby, mpId: 8}]
    endpoint: ym.stats_orders_changed
    tables: [ym_stats_orders]
  - id: product:lamoda.orders
    mode: active_product_spec
    inputMode: rolling_journal
    selection: latest_attempt_exact_run_covers_replay_window_no_fallback
    contract: {freshnessHours: 27, windowDays: null, endOffsetDays: 0, requiredScopes: []}
    endpoint: lamoda.orders
    tables: [lamoda_orders]
    # Дочерняя таблица позиций заказа (волна C). Ядро пока не читает.
    intake_only: [lamoda_orders_items]
  # Historical YM orders are raw payloads preserved by the legacy collector.
  # They supplement the fresh seven-day journal and can never substitute for it.
  - id: recovery:ym.stats_orders.legacy_recovery
    mode: legacy_recovery
    endpoint: ym.stats_orders.legacy_recovery
    tables: [ym_stats_orders]
    producer: marketplace-collector-v3/scripts/recover_ym_orders_from_legacy.py
    collectorClass: LegacyYmOrdersRecovery
    parserVersion: legacy-recovery-2026-08-04
    sourceBusinessKeyPrefix: "legacy:lamoda_reports.mp_orders_daily:"
    sourceBusinessKeyAction: order
    marketplaceIds: [6, 8]
    completenessGuard: recovered_historical_payload_not_live_collection
  - id: product:wb.returns
    mode: active_product_spec
    inputMode: rolling_journal
    selection: latest_attempt_exact_run_covers_replay_window_no_fallback
    contract: {freshnessHours: 27, windowDays: 7, endOffsetDays: 0, requiredScopes: []}
    endpoint: wb.returns
    tables: [wb_returns]
  # Historical recovery is deliberately not a ProductSpec run: its payload was
  # collected in the past and is preserved from legacy storage, not requested
  # from WB during this build.  It may supplement, never replace, the active
  # wb.returns input above.
  - id: recovery:wb.returns.legacy_recovery
    mode: legacy_recovery
    endpoint: wb.returns.legacy_recovery
    tables: [wb_returns]
    producer: marketplace-collector-v3/scripts/recover_wb_returns_from_legacy.py
    collectorClass: LegacyWbReturnsRecovery
    parserVersion: legacy-recovery-2026-08-04
    sourceBusinessKeyPrefix: "legacy:lamoda_reports.mp_returns:"
    sourceBusinessKeyAction: return
    marketplaceIds: [1, 2]
    completenessGuard: recovered_historical_payload_not_live_collection
  - id: product:ozon.returns
    mode: active_product_spec
    inputMode: rolling_journal
    selection: latest_attempt_exact_run_covers_replay_window_no_fallback
    contract: {freshnessHours: 27, windowDays: 14, endOffsetDays: 0, requiredScopes: []}
    endpoint: ozon.returns
    tables: [ozon_returns]
    # Дочерняя таблица из $.exemplars[] (волна C). Ядро пока не читает.
    intake_only: [ozon_returns_exemplars]
  - id: product:ym.returns
    mode: active_product_spec
    inputMode: rolling_journal
    selection: latest_attempt_exact_run_covers_replay_window_no_fallback
    contract:
      freshnessHours: 27
      windowDays: 30
      endOffsetDays: 0
      requiredScopes: [{key: fbs, mpId: 6}, {key: fby, mpId: 8}]
    endpoint: ym.returns
    tables: [ym_returns]
    # Дочерняя таблица из explode items[] — intake_only, ядро пока не читает.
    # Все четыре уровня разворота ym.returns: движок вложенного разворота пишет в
    # таблицы на каждом уровне, и контракт входов обязан знать про каждую. Пока тут
    # стоял только первый уровень, сборка падала с «ProductSpec/table mapping
    # differs» — 96 падений в манифесте и столько же в контракте (09.08.2026).
    intake_only: [ym_returns_items, ym_returns_items_decisions,
                  ym_returns_items_instances, ym_returns_items_tracks]
  # Historical recovery is deliberately not a ProductSpec run: its payload was
  # collected in the past and is preserved from legacy storage, not requested
  # from YM during this build.  It may supplement, never replace, the active
  # ym.returns input above.
  - id: recovery:ym.returns.legacy_recovery
    mode: legacy_recovery
    endpoint: ym.returns.legacy_recovery
    tables: [ym_returns]
    producer: marketplace-collector-v3/scripts/recover_ym_returns_from_legacy.py
    collectorClass: LegacyYmReturnsRecovery
    parserVersion: legacy-recovery-2026-08-03
    sourceBusinessKeyPrefix: "legacy:lamoda_reports.mp_returns:"
    sourceBusinessKeyAction: return
    marketplaceIds: [6, 8]
    completenessGuard: recovered_historical_payload_not_live_collection
  - id: ledger:ym.returns.acknowledged_missing_orders
    mode: acknowledged_loss_ledger
    ledgerId: ym.returns.acknowledged_missing_orders.v1
    path: specs/kernel/ym-acknowledged-missing-returns.json
    sourceEndpoint: ym.returns.legacy_recovery
    expectedEntryCount: 79
    sha256: 484cda6ab4ee16dd8cb38845f18ced6907c425f92c98a683d62483b9a4f9491e
    completenessGuard: named_finite_allowlist_with_file_digest
  - id: product:lamoda.status_dates
    mode: active_product_spec
    inputMode: rolling_journal
    selection: latest_attempt_exact_run_covers_replay_window_no_fallback
    contract: {freshnessHours: 27, windowDays: null, endOffsetDays: 0, requiredScopes: []}
    endpoint: lamoda.status_dates
    tables: [lamoda_status_dates]
    # Дочерняя таблица платежей (волна C). Ядро пока не читает.
    intake_only: [lamoda_status_dates_payments]
  - id: product:wb.analytics
    mode: active_product_spec
    inputMode: rolling_journal
    selection: latest_attempt_exact_run_covers_replay_window_no_fallback
    contract: {freshnessHours: 27, windowDays: 7, endOffsetDays: 0, requiredScopes: []}
    endpoint: wb.analytics
    tables: [wb_analytics]
  - id: product:ym.analytics
    mode: active_product_spec
    inputMode: rolling_journal
    selection: latest_attempt_exact_run_covers_replay_window_no_fallback
    contract: {freshnessHours: 27, windowDays: 7, endOffsetDays: 1, requiredScopes: []}
    endpoint: ym.analytics
    tables: [ym_analytics]

  # Historical facts produced from wb.stocks remain preserved, but a new
  # steady-state build may not read this disabled source. There is no sealed
  # baseline receipt which could make that fallback reproducible.
  - id: excluded:wb.stocks
    mode: excluded_product_spec
    endpoint: wb.stocks
    tables: [wb_stocks]
    reason: frozen historical facts only; wb.warehouse_stocks is authoritative

  # The ProductSpec still collects this FBS API, but current kernel return
  # semantics intentionally come from B2B lamoda.status_dates.
  - id: excluded:lamoda.returns
    mode: excluded_product_spec
    endpoint: lamoda.returns
    tables: [lamoda_return_boxes, lamoda_returns]
    reason: no live FBS return facts; kernel uses lamoda.status_dates

  - id: reference:onec_nomenclature_ref
    mode: external_reference
    inputMode: reference
    selection: latest_atomic_pair_receipt_no_fallback
    table: gwptd_intake.onec_nomenclature_ref
    producer: marketplace-collector-v3/scripts/refresh_onec_nomenclature.py
    completenessGuard: atomic_pair_snapshot_receipt
  # dim_warehouse is a manually governed kernel reference, not collector
  # output.  P1B-3 closes its executable relation exactly, but does not pretend
  # that its content is sealed: P1B-5 must bind it to a consistent generation.
  - id: kernel:dim_warehouse
    mode: kernel_static_reference
    table: gwptd_kernel.dim_warehouse
    consistency: caller_transaction_visibility
    completenessGuard: declared_relation_only_no_content_digest
  - id: kernel:v_fact_stocks_current
    mode: kernel_static_reference
    table: gwptd_kernel.v_fact_stocks_current
    consistency: caller_transaction_visibility
    completenessGuard: declared_relation_only_no_content_digest
  - id: external:cbr_usd_rub
    mode: prepared_external
    providerRef: kernel.etl.build_fact_rating:resolve_steady_state_rate
    completenessGuard: finite_positive_rate_with_bounded_fallback
  - id: formula:business_rules
    mode: formula_contract
    path: docs/domain/machine-rules/formulas.yaml
  - id: metric:cost_price
    mode: metric_spec
    metric: cost_price
    path: specs/metrics/cost_price.yaml
  - id: metric:rating
    mode: metric_spec
    metric: rating
    path: specs/metrics/rating.yaml
  - id: metric:turnover
    mode: metric_spec
    metric: turnover
    path: specs/metrics/turnover.yaml

steps:
  - name: dim_product
    mutationMode: upsert
    inputs:
      sources: [product:1c.supplier_catalog]
      derivedSteps: []
    exclusions: []
    outputs: [gwptd_kernel.dim_product]

  - name: resolve_identifiers
    mutationMode: upsert_and_quarantine_reconcile
    inputs:
      sources:
        - product:wb.cards_list
        - product:wb.orders
        - product:wb.sales
        - product:wb.orders_fbs
        - product:ozon.product_info_list
        - product:ozon.product_sku_map
        - product:ym.offer_mappings
        - product:lamoda.nomenclatures
        - reference:onec_nomenclature_ref
      derivedSteps: [dim_product]
    exclusions: []
    outputs:
      - gwptd_kernel.dim_product
      - gwptd_kernel.dim_product_identifier
      - gwptd_kernel.resolution_quarantine

  - name: wb_category
    mutationMode: upsert
    inputs:
      sources: [product:wb.cards_list]
      derivedSteps: [resolve_identifiers]
    exclusions: []
    outputs: [gwptd_kernel.dim_product_attribute]

  - name: fact_supplier_stock
    mutationMode: upsert
    inputs:
      sources: [product:1c.supplier_catalog]
      derivedSteps: [dim_product]
    exclusions: []
    outputs: [gwptd_kernel.fact_supplier_stock_price]

  - name: fact_stocks
    mutationMode: snapshot_reconcile
    inputs:
      sources:
        - product:wb.warehouse_stocks
        - product:wb.stocks_fbs
        - product:ozon.stocks
        - product:ozon.stock_on_warehouses
        - product:ym.stocks
        - product:lamoda.inventory
        - kernel:v_fact_stocks_current
      derivedSteps: [resolve_identifiers]
    exclusions: [excluded:wb.stocks]
    outputs: [gwptd_kernel.fact_stocks_daily, gwptd_kernel.resolution_quarantine]

  - name: fact_prices
    mutationMode: upsert
    inputs:
      sources: [product:wb.prices, product:ozon.prices, product:ym.prices]
      derivedSteps: [resolve_identifiers]
    exclusions: []
    outputs: [gwptd_kernel.fact_prices_daily]

  - name: fact_storage
    mutationMode: snapshot_reconcile
    inputs:
      sources: [product:wb.paid_storage, product:ym.storage]
      derivedSteps: [resolve_identifiers]
    exclusions: []
    outputs: [gwptd_kernel.fact_storage_costs]

  - name: fact_finance
    mutationMode: scope_replace_and_upsert
    inputs:
      sources: [product:wb.finance_reports, product:ozon.finance_transactions, product:ym.finance]
      derivedSteps: [resolve_identifiers]
    exclusions: []
    # resolution_quarantine добавлен 10.08.2026: SKU, не разрешившийся ни
    # действующей, ни исторической связкой, уходит сюда с
    # reason='fact_finance_unresolved' — инвариант A3 (zero-drop),
    # docs/domain/IDENTITY-RESOLUTION.md §4.6. Раньше такой SKU ронял ВЕСЬ
    # прогон, а публикация когерентная: заказы считаются после финансов, поэтому
    # одна строка на -12 ₽ по товару, снятому с площадки, морозила заказы,
    # рейтинг и оборачиваемость на восемь дней. Так же объявляет карантин
    # fact_orders — образец не новый.
    outputs:
      - gwptd_kernel.fact_finance
      - gwptd_kernel.resolution_quarantine

  - name: fact_orders
    mutationMode: snapshot_reconcile
    inputs:
      sources:
        - product:wb.orders
        - product:wb.sales
        - product:wb.orders_fbs
        - product:ozon.postings_fbo
        - product:ozon.postings_fbs
        - product:ozon.postings_fbs_changed
        - product:ym.stats_orders
        - product:ym.stats_orders_changed
        - recovery:ym.stats_orders.legacy_recovery
        - product:lamoda.orders
        # Освежает статус заказов Lamoda пост-шагом LAMODA_STATUS_REFRESH_SQL.
        # lamoda.orders обрывается на первом заказе старше 7 дней (itemStop), а
        # lamoda.status_dates бьёт в тот же /api/v1/orders БЕЗ обрыва и потому
        # знает текущий статус каждого заказа. Замер 03.08: 1329 расхождений.
        - product:lamoda.status_dates
        - product:ozon.finance_transactions
      derivedSteps: [resolve_identifiers]
    exclusions: []
    outputs:
      - gwptd_kernel.fact_orders
      - gwptd_kernel.fact_order_state_history
      - gwptd_kernel.resolution_quarantine

  - name: fact_returns
    mutationMode: snapshot_reconcile
    inputs:
      sources:
        - product:wb.returns
        - recovery:wb.returns.legacy_recovery
        - product:ozon.returns
        - product:ym.returns
        - recovery:ym.returns.legacy_recovery
        - ledger:ym.returns.acknowledged_missing_orders
        - product:lamoda.status_dates
      derivedSteps: [resolve_identifiers, fact_orders]
    exclusions: [excluded:lamoda.returns]
    outputs: [gwptd_kernel.fact_returns]

  - name: fact_analytics
    mutationMode: upsert
    inputs:
      sources: [product:wb.analytics, product:ym.analytics]
      derivedSteps: [resolve_identifiers]
    exclusions: []
    outputs: [gwptd_kernel.fact_analytics]

  - name: dedupe_cross_mp_orders
    mutationMode: dedupe_delete
    inputs:
      sources: []
      derivedSteps: [fact_orders]
    exclusions: []
    outputs: [gwptd_kernel.fact_orders]

  - name: fact_turnover
    mutationMode: derived_snapshot_replace
    inputs:
      sources: [formula:business_rules, metric:turnover]
      derivedSteps:
        - dedupe_cross_mp_orders
        - fact_returns
        - fact_stocks
        - fact_storage
    exclusions: []
    outputs: [gwptd_kernel.fact_turnover]

  - name: fact_rating
    mutationMode: derived_snapshot_replace
    inputs:
      sources:
        - external:cbr_usd_rub
        - formula:business_rules
        - metric:cost_price
        - metric:rating
      derivedSteps:
        - dim_product
        - fact_supplier_stock
        - fact_stocks
        - fact_returns
        - dedupe_cross_mp_orders
    exclusions: []
    outputs: [gwptd_kernel.fact_rating]

  - name: validate_invariants
    mutationMode: validation_only
    inputs:
      sources:
        - kernel:dim_warehouse
        - formula:business_rules
        - metric:cost_price
        - metric:rating
        - metric:turnover
      derivedSteps:
        - fact_stocks
        - dedupe_cross_mp_orders
        - fact_turnover
        - fact_rating
    exclusions: []
    outputs: [validation:kernel_invariants]
