Aller au contenu

Feed normalization rules architecture

Purpose

The normalization pipeline must remain extensible as new merchants and product families are added. 90-offers-norm.php is an orchestrator, not the long-term home for merchant-specific business rules.

The central rule is:

  • product knowledge belongs to product/category modules;
  • feed/merchant quirks belong to norm/feed-rules/<feed>/;
  • orchestration and execution ordering remain in the pipeline stage that owns the timing.

This separation prevents a new merchant from turning the shared normalization pipeline into another monolith.

Merchant vs product rule

A rule belongs to a merchant module when its correctness depends on the source feed itself: feed-specific model conventions, malformed categories, merchant-only accessory mappings, known source-media corrections, source-specific fallbacks, or final locks needed only for that feed.

A rule stays generic when it describes the product independently of the merchant. Examples include recognizing a smartphone, laptop, monitor, oven, dishwasher, projector, printer, or other product family from evidence that is valid across feeds.

Brand names alone do not make a rule merchant-specific. For example, generic Galaxy smartphone recognition remains a product rule unless it explicitly depends on the Samsung source feed.

Directory convention

Merchant-specific rules use:

plugins/ccx-feeds-industrial/includes/pipeline/norm/feed-rules/
├── acer/
│   ├── normalization/
│   └── finalize/
└── samsung/
    └── finalize/

Create subdirectories only when real rules exist. Do not create empty architecture for hypothetical work.

Large product families should remain split into focused category modules instead of being replaced by one huge file per category.

Execution-order contract

Externalizing code must not silently change pipeline ordering.

When a merchant rule is extracted from 90-offers-norm.php, prefer:

  1. move the implementation into the merchant module;
  2. keep the call site at the original pipeline position;
  3. guard the merchant implementation by its exact source_feed when the previous behavior is demonstrably merchant-specific;
  4. preserve SQL predicates and update order unless a separate behavioral change is intentionally reviewed and tested.

Never move a collection of rules to a convenient new stage merely because they share a merchant name.

Acer status

Acer uses the runtime feed key acer.

Current dedicated rule areas include:

norm/feed-rules/acer/normalization/10-accessories.php
norm/feed-rules/acer/normalization/20-upsert-projection.php
norm/feed-rules/acer/normalization/30-image-gallery.php
norm/feed-rules/acer/finalize/10-runtime-fixes.php
norm/feed-rules/acer/finalize/20-projector-model.php
norm/feed-rules/acer/finalize/30-category-policy.php

10-runtime-fixes.php contains Acer-only implementations that were previously inline in 90-offers-norm.php. Their call sites remain at their original positions so stage ordering is preserved.

30-category-policy.php owns Acer's final category acceptance/filtering policy. Its call site intentionally remains at the historical end-of-normalization position, after final status/coherence and SEO backfill; moving it into an earlier merchant-runner phase would change behavior.

Generic laptop, monitor, projector, accessory, GPU, CPU, and other product knowledge must not be copied into Acer merely because Acer sells those products.

Samsung status

Samsung uses the runtime feed key samsung and is registered as an active Awin feed.

Dedicated Samsung rules currently begin with:

norm/feed-rules/samsung/finalize/10-appliance-model-locks.php

This module isolates source-specific Samsung appliance model signatures such as the relevant NZ, NV/NQ, MS/MG/MC, VS, DW, and DV conventions and Samsung Jet handling.

Generic appliance recognition by title/category remains in shared product rules. Generic Samsung/Galaxy brand recognition also remains shared when it is not dependent on source_feed = samsung.

Further Samsung exceptions should be migrated one by one only after proving that they are source-specific.

The known DW60CG550B00ET dishwasher media mismatch is also merchant-specific: only that source-media repair belongs to Samsung. Dishwasher color normalization remains generic product logic.

Adding a merchant

For a new merchant:

  1. register and confirm the exact runtime source_feed key;
  2. import enough real data to understand the feed rather than pre-building speculative rules;
  3. keep universal product classification in the product/category layer;
  4. create norm/feed-rules/<source_feed>/ only for real feed-specific behavior;
  5. preserve the existing pipeline stage/order when extracting implementations;
  6. add characterization/contract tests for behavior that could regress.

Do not create merchant modules for merchants that have not yet been imported merely to make the directory tree look complete.

Validation gate

A normalization refactor is not complete until all relevant checks remain green. At minimum:

composer test

and the repository audit scripts applicable to the changed area must pass.

During the Acer/Samsung extraction work, the baseline is 638 PHPUnit tests. A refactor must not be accepted merely because PHP syntax is valid.

For frontend-affecting changes, the frontend audit must also remain compliant (no executable inline scripts/styles, asset-size limits respected, and version markers present).

Current refactor direction

The long-term target is for 90-offers-norm.php to become progressively smaller and primarily coordinate stages and calls. Merchant names should disappear from the orchestrator where their implementation can safely live behind dedicated feed-rule functions.

This is incremental work: extract proven merchant-specific behavior first, keep generic category logic generic, preserve execution order, run the full test/audit gate, then continue with the next isolated rule.

The Samsung NQ exclusion used by the shared microwave title classifier is implemented by the Samsung module; the generic orchestrator no longer owns that Samsung model signature.

Acer projection and media boundary

Acer source-feed fallbacks used during the initial offers_norm projection (brand fallback, Aspire/Veriton model extraction, Acer accessory hints, series, CPU/GPU and OS fallbacks) live in norm/feed-rules/acer/normalization/20-upsert-projection.php. The shared upsert keeps only generic product/brand logic and interpolates the Acer fragments at the original SQL positions.

Acer ProductServe/static2 image URL repair lives in norm/feed-rules/acer/normalization/30-image-gallery.php. norm/20-image-gallery.php now owns only the generic media-quality pass. The Acer repair still runs immediately before that generic pass, preserving execution order.

Smartphone final-lock boundary

The final smartphone classification locks (CCX_SMARTPHONE_HARD_LOCK_V1/V2/V3 and the smartphone/audio false-positive guard) live in norm/26-smartphone-final-locks.php. They are generic product-family knowledge, not merchant rules. 90-offers-norm.php keeps a single call at the original execution position so later appliance/final locks keep the same ordering.

Generic home-appliance final locks

Late generic classification and normalization for cooktop, oven, microwave, vacuum, dishwasher, dryer, and freezer lives in norm/27-home-appliance-final-locks.php. The orchestrator calls this module at the original position. Samsung model/media functions remain implemented under feed-rules/samsung/ but their call-sites stay interleaved inside the category module so the established generic/Samsung execution order is unchanged.

Active merchant scope

The current industrialized merchant scope is deliberately limited to Acer and Samsung. No other merchant is considered active architecture work at this stage.

This distinction is intentional:

  • active merchant = imported, supported, tested and allowed to execute merchant-specific normalization;
  • legacy merchant code = historical implementation awaiting dependency review/removal;
  • generic product knowledge = reusable classification/model logic that remains valid regardless of merchant and must not be deleted merely because a historical feed used it.

Before adding merchant #3, the repository must have no active execution path for unimplemented merchants, all remaining legacy code must be classified, and documentation must match the actual runtime scope.

Legacy removal policy

Legacy merchant code is removed in dependency-safe batches. A file can be deleted only after proving it is not required by Acer/Samsung runtime behavior, generic product classification, or a still-valid characterization test. Each removal batch must run the full PHPUnit suite and canonical repository audit and update this document when architecture changes.

Legacy merchant physical cleanup

Dedicated normalization and mapping modules for inactive legacy merchants have been removed from the active repository after the normalization runtime was restricted to Acer and Samsung. Historical migration inventories may retain old paths as migration records; they are not active architecture.

Legacy post-map removal

norm/legacy-post-map.php was removed after the active merchant scope was reduced to Acer and Samsung. The file contained historical Darty/Electro Depot post-map repairs only; a Samsung product-title token occurred inside those legacy merchant repairs and was not a Samsung feed rule. The active normalization orchestrator no longer loads or calls this legacy stage.

Classification-lock legacy pruning

norm/80-classification-locks.php now retains only the generic CCX_INDUSTRIAL_PHONE_ACCESSORY_LOCK_V1 behavior used by the active catalog pipeline. Historical Electro Depot and Darty classification-lock branches were removed because those merchants are outside the current industrialized scope (Acer + Samsung). Generic product knowledge remains separate from merchant-specific rules.

Active feed registry boundary

includes/feeds-registry.php is intentionally limited to the two industrialized merchant feeds: samsung and acer. Runtime activation through CCX_ACTIVE_FEEDS is filtered against this supported set, so historical or mistyped feed keys cannot reactivate legacy merchants. Legacy merchant registry definitions/filters are not part of the current runtime registry. New merchants must be added explicitly only after their feed-specific rules, tests and documentation are ready.

Batch persister merchant-category boundary

runtime/batch-persister.php no longer contains historical Darty/Pixmania/Rakuten category allowlists. Merchant-specific raw-category filtering belongs upstream in the active feed registry/input layer; the persister only honours an available shared filter and otherwise performs generic persistence. This keeps the runtime persistence layer independent from inactive merchant taxonomies while preserving Acer/Samsung persistence behavior.

Batch persister deep merchant cleanup

runtime/batch-persister.php is merchant-neutral for inactive feeds inside upsertOffersFinal(): legacy merchant branches, locks and their historical markers are removed. Generic persistence and gaming normalization remain, together with all active Acer runtime corrections. Future merchant-specific intelligence must be implemented upstream in dedicated merchant modules.

Dedicated Fnac mapping modules removed

The inactive Fnac provider mapping modules under includes/mapping/providers/fnac/ were removed together with their direct includes and call-sites from common/row-final-builder.php. Acer/Samsung product-family logic remains in shared code where it is merchant-neutral or explicitly active.

Fnac feed integration removed

The inactive Fnac feed integration has been removed from the current runtime architecture: the legacy registry overlay, feed environment declarations, parser placeholder/debug plumbing and feed-specific classification branches are gone. Fnac is neither runtime-active nor part of the prepared feed catalogue. Generic merchant-name and title normalization may still recognize Fnac when it appears as data from another source; this does not constitute a feed integration or activation right.

Effinity mapping capability retained for future feeds

Effinity support remains available as platform capability for the planned electrodepot, certideal and pc_portable_net feeds. These feeds are not active in the current Acer/Samsung runtime registry, but their shared Effinity header mapping, raw-field capture, scope bypass and fallback identity behavior are intentionally preserved so those integrations can be activated later without rebuilding the provider adapter.

Prepared feeds cannot execute syncs

Feed configuration and feed activation are separate concerns. Future merchant connectors may be declared as status=prepared with enabled=false, but ccx_sync_runner_run_feed() fails closed with feed_not_active unless the feed is explicitly enabled by the deployment-owned CCX_ACTIVE_FEEDS authority. This guard runs before runner construction, resume lookup, download or parsing, so admin/HTTP/worker/manual entry points cannot accidentally execute a prepared merchant. Queue enqueueing already filters on the same runtime activation authority.

Canonical prepared-feed catalogue

includes/prepared-feed-catalogue.php is the declaration source for connectors known by the platform. feeds-registry.php materializes those definitions as status=prepared / enabled=false unless a feed is explicitly authorized by CCX_ACTIVE_FEEDS. Declaring a connector therefore does not schedule, download, parse, resume, or sync it. Provider parsing (AWIN/Effinity) may be prepared ahead of merchant onboarding, while merchant-specific classification rules remain deferred until that merchant is audited and activated.

Prepared feed catalogue and snapshots

The prepared feed catalogue is the canonical declaration source for known feed connectors. Snapshot storage derives its supported feed keys from that catalogue instead of maintaining merchant-specific allowlists. Catalogue membership is descriptive only and never activates execution; CCX_ACTIVE_FEEDS remains the sole runtime activation authority.

Finished feeds runtime-active scope

The operational finished-feeds administration view is scoped exclusively to ccx_feed_runtime_active_feeds(). Prepared catalogue entries remain inspectable through catalogue/health tooling but are not treated as operational feeds until explicitly enabled through CCX_ACTIVE_FEEDS.

Sync health active-feed scope

Sync-health keeps the full registry only as catalogue metadata (registered_feeds / disabled_feeds). Operational per-feed quality, scoring and enabled status derive exclusively from ccx_feed_runtime_active_feeds(). An empty active list remains empty and never falls back to every prepared feed. Duplicate per-feed scoring was removed so one canonical scoring pass remains.

Sync health prepared vs active scope

ccx_sync_health_registry_feeds() may expose all prepared/registered connectors as inventory metadata. Operational health scoring, per-feed quality metrics and enabled/disabled status derive only from ccx_feed_runtime_active_feeds(). An empty active list is authoritative and must never fall back to the full prepared registry. CLI inspection may read prepared registry entries without granting execution rights.

Feed inspector prepared status

The CLI feed inspector is an inspection/readiness tool and may inspect prepared catalogue entries. Registry membership alone is not reported as active: active requires the registry entry to be enabled with active status; other declared entries are reported as prepared. The inspector no longer carries a Darty-specific source fallback because prepared source resolution belongs to the canonical registry/catalogue.

Inspection is an explicit operator action and may download the remote source of a prepared feed to a local temporary spool for read-only analysis. This network read does not grant runtime execution rights: prepared feeds remain excluded from scheduling, queueing, synchronization and mutating reconciliation until explicitly activated through the runtime-active authority.

Queue runtime-active scope

Queue scheduling iterates only the feed keys returned by the deployment-managed runtime-active authority. The prepared registry is consulted only for configuration of those active keys; catalogue membership alone can never enqueue a feed. An empty runtime-active set does not fall back to prepared feeds.

Sync health operational scope

Sync-health may inventory every registered/prepared feed as catalogue metadata, but operational per-feed quality, scoring and enabled/disabled state derive exclusively from the runtime-active feed authority. An empty runtime-active set never falls back to all prepared feeds.

Health reconciliation scope

Health inventory/readiness may enumerate prepared feed definitions, but mutating reconciliation actions are scoped to ccx_feed_runtime_active_feeds() only. A prepared feed must never be inserted/refreshed by the global reconcile action until it is explicitly activated.