Aller au contenu

BC-064A — Projection / Application Boundary Audit

Status: HISTORICAL For the current architecture, see: ./dependances.md, ../projections/overview.md, ../frontend/projections.md.

Status

Audit only.

No functional change, no class move, no namespace rename, no runtime sync.

Scope

  • src/Domain/
  • src/Contracts/
  • src/Application/
  • src/Projection/
  • src/ReadService/
  • src/Adapter/
  • src/Infrastructure/
  • plugin frontend/application/runtime entrypoints that compose these classes

Normative target

Source of truth used for this audit:

  • docs/architecture/boundary-architecture.md
  • docs/architecture/dependances.md
  • docs/architecture/principes.md
  • docs/architecture/application-infrastructure-contract.md

Target direction retained for BC-064:

Layer May depend on
Domain internal helpers, internal contracts
Contracts no implementation detail
Application Domain, Knowledge, Contracts
Projection Domain, Contracts, pure projection DTOs
ReadService Domain, Contracts, Vertical Modules, Projection DTOs
Infrastructure Contracts
Adapter Contracts, Infrastructure, legacy tech details

Practical corollaries for this audit:

  • Projection must not depend on Application orchestration DTOs.
  • ReadService must not instantiate concrete adapters/shims by default.
  • Infrastructure must not depend on Application-owned ports/DTOs.
  • Contracts should not be forced to import specialized Projection DTOs when a more stable contract shape is expected.

Observed dependency signals

Coarse static scan on the audited tree:

Edge Count Observation
Projection -> Application 1 real violation
ReadService -> Adapter 4 real boundary drift
Infrastructure -> Application 3 real boundary drift
Contracts -> Projection 8 stable-contract fragility

Namespace graph

Domain ───────────────────────────────► Domain
Contracts ────────────────────────────► Projection      (fragile)
Application ──────────────────────────► Domain
Projection ───────────────────────────► Application     (forbidden)
ReadService ──────────────────────────► Contracts
ReadService ──────────────────────────► Projection
ReadService ──────────────────────────► Adapter         (forbidden)
Adapter ──────────────────────────────► Contracts
Adapter ──────────────────────────────► Projection
Infrastructure ───────────────────────► Contracts       (target)
Infrastructure ───────────────────────► Application     (forbidden)
Plugin frontend ──────────────────────► ReadService / Contracts / Projection
Adapter legacy read ──────────────────► WordPress globals / plugin helpers / SQL

Detailed findings

BA-PA-001 — Projection depends on Application

  • Priority: P0
  • Type: real boundary violation
  • Edge: Projection -> Application
  • File: src/Projection/Offer/OfferProjectionBuilder.php
  • Proof: imports CMonChoix\Platform\Application\Enrichment\EnrichedStageEntry

Observed shape:

  • OfferProjectionBuilder::fromEnrichedStageEntry(EnrichedStageEntry $entry): array
  • OfferProjectionBuilder::fromRecord(EnrichedStageEntry $entry, array $record, int $index): OfferProjection

Why this is invalid:

  • EnrichedStageEntry is an Application orchestration DTO.
  • Projection becomes aware of a use-case transport shape instead of a stable projection input.
  • This inverts the intended direction: Application may prepare data for Projection, but Projection must not import Application.

Blast radius:

  • src/Application/Enrichment/EnrichedStageEntry.php
  • src/Application/DomainCore/BuildCandidateFromEnrichedStageEntry.php
  • tests around projection building and enrichment

Recommended BC:

  • BC-064B

Status after BC-064B:

  • closed by introducing CMonChoix\Platform\Contracts\Projection\OfferProjectionSource
  • EnrichedStageEntry now implements the neutral contract
  • OfferProjectionBuilder no longer imports Application\Enrichment\EnrichedStageEntry

BA-RA-001 — ReadService instantiates concrete legacy adapters

  • Priority: P1
  • Type: real boundary violation
  • Edge: ReadService -> Adapter
  • File: src/ReadService/ProductModels/ProductModelsFrontendReadService.php

Proof:

  • default constructor values instantiate:
  • new LegacyCatalogItemProjectionAdapter()
  • new LegacyFacetProjectionAdapter()
  • new LegacyProductProjectionAdapter()

Why this is invalid:

  • ReadService should stay on stable contracts and projection DTOs.
  • Concrete legacy adaptation belongs to the adapter composition boundary, not inside the read service itself.
  • This makes the read service hard to reuse outside the legacy WordPress rendering path.

Blast radius:

  • plugins/ccx-feeds-industrial/includes/frontend/product-models.php
  • runtime mirror
  • tests/Runtime/ProductModelsProjectionBuilderTest.php
  • tests/Frontend/ProductModelsCatalogRoutingTest.php

Recommended BC:

  • BC-064C

Status after BC-064C:

  • closed by injecting CatalogItemProjectionMapper, FacetProjectionMapper and ProductViewProjectionMapper
  • concrete legacy adapter composition moved to WordPress frontend wrappers

BA-RA-002 — ReadService depends on array shim

  • Priority: P2
  • Type: controlled transitional coupling
  • Edge: ReadService -> Adapter
  • File: src/ReadService/Frontend/PublicNavigationReadService.php

Proof:

  • constructor default value: new FrontendProjectionArrayShim()

Why this is fragile:

  • the shim itself is acceptable as an explicit compatibility adapter;
  • the fragile point is its placement inside ReadService rather than at the outer adapter/rendering boundary.

Why this is lower priority than BA-RA-001:

  • it remains read-only;
  • it converts projection DTOs to a legacy array shape;
  • it does not pull SQL or WordPress execution into ReadService.

Recommended BC:

  • BC-064C

Status after BC-064C:

  • closed by making PublicNavigationReadService return NavigationProjection
  • FrontendProjectionArrayShim remains confined to the WordPress wrapper

BA-IA-001 — Infrastructure depends on Application-owned feed ports

  • Priority: P1
  • Type: real boundary violation
  • Edge: Infrastructure -> Application

Files:

  • src/Infrastructure/Feed/InMemoryMerchantFeedPublicationInputReader.php
  • src/Infrastructure/Feed/InMemoryRawMerchantFeedBatchWriter.php

Proof:

  • imports from CMonChoix\Platform\Application\Feed\...
  • implements:
  • MerchantFeedPublicationInputReader
  • RawMerchantFeedBatchWriter
  • returns MerchantFeedPublicationInput

Why this is invalid:

  • normative docs state Infrastructure may depend on Contracts.
  • here the ports and one crossing DTO are owned by Application.
  • Infrastructure therefore imports an application boundary instead of a stable contract boundary.

Recommended BC:

  • BC-064D

BA-CP-001 — Contracts import specialized Projection DTOs

  • Priority: P1
  • Type: contract fragility
  • Edge: Contracts -> Projection

Files:

  • src/Contracts/Projection/CanonicalProductProjectionReader.php
  • src/Contracts/Projection/CatalogProjectionReader.php
  • src/Contracts/Projection/NavigationProjectionReader.php
  • src/Contracts/Projection/ProductProjectionReader.php

Proof:

  • these interfaces import DTOs from src/Projection/...

Why this is fragile:

  • docs describe Contracts as independent and free of implementation detail;
  • current interfaces are stable enough for runtime use, but they are not neutral contracts;
  • they couple the collaboration boundary to one specialized projection namespace.

Assessment:

  • not the first fix to land;
  • but this is the structural reason later layers cannot fully decouple cleanly.

Recommended BC:

  • BC-064D or BC-064F depending on desired extraction strategy

BA-AL-001 — Legacy namespace island remains inside src/Application

  • Priority: P2
  • Type: architectural debt / coexistence risk

Proof:

  • multiple files still use namespace CMonChoix\Application\...
  • examples:
  • src/Application/Projection/ProjectionCatalogWiring.php
  • src/Application/Contracts/EventStoreInterface.php
  • src/Application/Contracts/QueueInterface.php
  • src/Application/Catalog/MerchantNormalizer.php
  • some src/Infrastructure/... classes import these legacy namespaces

Why this matters:

  • the tree mixes CMonChoix\Platform\... and legacy CMonChoix\Application\...
  • dependency audits become harder because physical location and logical layer no longer align

Assessment:

  • not an immediate runtime regression;
  • but it amplifies BC-064 remediation cost and hides future boundary leaks.

Recommended BC:

  • BC-064E

BA-AL-002 — Application bridge still uses require_once

  • Priority: P2
  • Type: transitional compatibility bridge
  • File: src/Application/Normalization/NormalizationFacade.php

Proof:

  • require_once __DIR__ . '/../Catalog/MerchantNormalizer.php';
  • imports legacy namespace CMonChoix\Application\Catalog\MerchantNormalizer

Why this is fragile:

  • Application keeps a file-level legacy bootstrap bridge;
  • this is not a business bug, but it prevents a clean Application/Contracts boundary story.

Recommended BC:

  • BC-064E

Interfaces and ports inventory

Stable contracts currently under src/Contracts

File Role
src/Contracts/Projection/CanonicalProductProjectionReader.php projection read port
src/Contracts/Projection/CatalogProjectionReader.php projection read port
src/Contracts/Projection/NavigationProjectionReader.php projection read port
src/Contracts/Projection/ProductProjectionReader.php projection read port

Ports still owned by src/Application

File Current role Expected direction issue
src/Application/Feed/MerchantFeedPublicationInputReader.php input port consumed by Infrastructure
src/Application/Feed/RawMerchantFeedBatchWriter.php output port consumed by Infrastructure
src/Application/Contracts/EventStoreInterface.php legacy port namespace island
src/Application/Contracts/QueueInterface.php legacy port namespace island
src/Application/Shared/Event/EventDispatcherInterface.php legacy/shared port namespace island

DTO inventory relevant to the boundary

File Role Observation
src/Application/Enrichment/EnrichedStageEntry.php orchestration DTO wrongly imported by Projection
src/Application/Normalization/NormalizedStageEntry.php orchestration DTO stays in Application for now
src/Application/Feed/MerchantFeedPublicationInput.php feed crossing DTO better placed with boundary contracts
src/Application/Feed/ReceiveMerchantFeedResult.php use-case result DTO acceptable in Application
src/Projection/... DTOs read model DTOs acceptable in Projection

Concrete instantiations found in ReadService / Application

File Instantiation / bridge Assessment
src/ReadService/ProductModels/ProductModelsFrontendReadService.php 3 concrete legacy adapters in constructor defaults forbidden drift
src/ReadService/Frontend/PublicNavigationReadService.php new FrontendProjectionArrayShim() transitional drift
src/Application/Normalization/NormalizationFacade.php require_once legacy bridge transitional drift
src/Application/M24/Daemon/WorkerDaemon.php function_exists('pcntl_*') acceptable executable concern

No direct $wpdb usage was found in src/Application or src/ReadService.

Legacy shim inventory

File Purpose Audit result
src/Adapter/Legacy/Projection/FrontendProjectionArrayShim.php DTO -> legacy array translation acceptable as adapter, not inside ReadService
src/Adapter/Legacy/Projection/LegacyCatalogItemProjectionAdapter.php legacy item mapping acceptable adapter
src/Adapter/Legacy/Projection/LegacyFacetProjectionAdapter.php legacy facet mapping acceptable adapter
src/Adapter/Legacy/Projection/LegacyNavigationProjectionAdapter.php legacy nav mapping acceptable adapter
src/Adapter/Legacy/Projection/LegacyProductProjectionAdapter.php legacy product mapping acceptable adapter

Builders audit

File Result
src/Projection/Offer/OfferProjectionBuilder.php forbidden Projection -> Application dependency
src/Domain/Product/Cluster/ProductClusterBuilder.php domain-local builder, no BC-064A issue found
src/Application/Projection/CatalogProjectionWriter.php application write-side service, outside the primary projection-read leak found here

Cycles and near-cycles

No direct PHP namespace cycle was proven by this audit.

Two structural near-cycles were proven:

  1. Application DTO -> Projection builder, while Contracts already point to Projection DTOs.
  2. ReadService -> Adapter, while plugin frontend already composes the same read path from the outside.

These patterns are enough to make future cycles easy to introduce even when the current code still runs.

src ↔ plugin legacy coupling

Acceptable direction

Plugin/frontend code composes Platform classes:

  • catalog router uses CatalogProjectionReader and CatalogQuery
  • product canonical repository uses CanonicalProductReadService
  • public navigation uses PublicNavigationReadService
  • product models frontend uses ProductModelsFrontendReadService

This direction is expected: WordPress adapter -> Platform classes.

Controlled legacy coupling inside adapters

The following adapter readers still use WordPress/plugin helpers and SQL:

  • src/Adapter/Legacy/Read/SqlNavigationProjectionReader.php
  • src/Adapter/Legacy/Read/SqlLegacyCatalogProjectionReader.php
  • src/Adapter/Legacy/Read/SqlCanonicalProductProjectionReader.php

Observed concrete couplings:

  • $GLOBALS['wpdb']
  • sanitize_title()
  • remove_accents()
  • ccx_feeds_table()
  • ccx_feeds_db_has_table()
  • ccx_navigation_taxonomy_*
  • ccx_catalog_nav_cache_*

Assessment:

  • acceptable in Adapter layer;
  • forbidden if similar calls move upward into ReadService, Projection or Application.

Existing test coverage gap

Current architecture tests do not fully guard BC-064 concerns:

  • tests/Architecture/ProjectionBoundaryTest.php forbids WordPress/runtime/frontend/SQL leaks in Projection, but does not forbid Projection -> Application;
  • tests/Architecture/FrontendReadServiceBoundaryTest.php characterizes plugin/frontend delegation, but does not forbid ReadService -> Adapter.

Priority matrix

ID Cause Layer Severity Classification Action
BA-PA-001 Projection imports orchestration DTO Projection/Application P0 real violation extract neutral projection input
BA-RA-001 ReadService instantiates legacy adapters ReadService/Adapter P1 real violation move adapter wiring outward
BA-RA-002 ReadService owns array shim ReadService/Adapter P2 transitional drift move shim outward after BA-RA-001
BA-IA-001 Infrastructure imports Application feed ports Infrastructure/Application P1 real violation move feed ports/DTO contract boundary
BA-CP-001 Contracts import Projection DTOs Contracts/Projection P1 structural fragility introduce neutral contracts progressively
BA-AL-001 legacy namespace island under src/Application Application P2 legacy coexistence debt isolate/retire
BA-AL-002 require_once bridge in Application Application P2 transitional compatibility replace after namespace isolation

Proposed sequencing

BC-064B — Projection input neutralization

  • remove Projection -> Application
  • replace EnrichedStageEntry dependency in OfferProjectionBuilder
  • add architecture gate forbidding use CMonChoix\\Platform\\Application\\ inside src/Projection

BC-064C — ReadService adapter extraction

  • externalize concrete adapter/shim composition from ReadService
  • keep behavior unchanged for WordPress/frontend runtime
  • add gate forbidding new .*Adapter() and new .*Shim() in src/ReadService

BC-064D — Contract boundary extraction

  • move feed ports and crossing DTOs to stable contracts
  • decide whether projection reader contracts stay in Contracts/Projection or become neutral read contracts returning neutral DTOs

BC-064E — Legacy namespace island isolation

  • isolate remaining CMonChoix\Application\... subtree
  • remove file-level bridges like require_once from NormalizationFacade

BC-064F — Final certification

  • add architecture tests for:
  • no Projection -> Application
  • no ReadService -> Adapter
  • no Infrastructure -> Application
  • optionally, controlled Contracts -> Projection policy

Risks if left unchanged

  • Projection remains coupled to one use-case orchestration path.
  • Read services remain bound to WordPress legacy rendering concerns.
  • Infrastructure contracts remain hard to reuse or replace.
  • future extractions will cost more because logical layer and physical tree diverge.

Rollback

Audit-only rollback is trivial:

  • remove this document.

No SQL rollback. No runtime rollback. No sync rollback.