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.mddocs/architecture/dependances.mddocs/architecture/principes.mddocs/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): arrayOfferProjectionBuilder::fromRecord(EnrichedStageEntry $entry, array $record, int $index): OfferProjection
Why this is invalid:
EnrichedStageEntryis 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.phpsrc/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 EnrichedStageEntrynow implements the neutral contractOfferProjectionBuilderno longer importsApplication\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.phptests/Frontend/ProductModelsCatalogRoutingTest.php
Recommended BC:
- BC-064C
Status after BC-064C:
- closed by injecting
CatalogItemProjectionMapper,FacetProjectionMapperandProductViewProjectionMapper - 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
PublicNavigationReadServicereturnNavigationProjection FrontendProjectionArrayShimremains 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.phpsrc/Infrastructure/Feed/InMemoryRawMerchantFeedBatchWriter.php
Proof:
- imports from
CMonChoix\Platform\Application\Feed\... - implements:
MerchantFeedPublicationInputReaderRawMerchantFeedBatchWriter- 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.phpsrc/Contracts/Projection/CatalogProjectionReader.phpsrc/Contracts/Projection/NavigationProjectionReader.phpsrc/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.phpsrc/Application/Contracts/EventStoreInterface.phpsrc/Application/Contracts/QueueInterface.phpsrc/Application/Catalog/MerchantNormalizer.php- some
src/Infrastructure/...classes import these legacy namespaces
Why this matters:
- the tree mixes
CMonChoix\Platform\...and legacyCMonChoix\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:
Application DTO -> Projection builder, while Contracts already point to Projection DTOs.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
CatalogProjectionReaderandCatalogQuery - 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.phpsrc/Adapter/Legacy/Read/SqlLegacyCatalogProjectionReader.phpsrc/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.phpforbids WordPress/runtime/frontend/SQL leaks in Projection, but does not forbidProjection -> Application;tests/Architecture/FrontendReadServiceBoundaryTest.phpcharacterizes plugin/frontend delegation, but does not forbidReadService -> 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
EnrichedStageEntrydependency inOfferProjectionBuilder - add architecture gate forbidding
use CMonChoix\\Platform\\Application\\insidesrc/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()andnew .*Shim()insrc/ReadService
BC-064D — Contract boundary extraction¶
- move feed ports and crossing DTOs to stable contracts
- decide whether projection reader contracts stay in
Contracts/Projectionor become neutral read contracts returning neutral DTOs
BC-064E — Legacy namespace island isolation¶
- isolate remaining
CMonChoix\Application\...subtree - remove file-level bridges like
require_oncefromNormalizationFacade
BC-064F — Final certification¶
- add architecture tests for:
- no
Projection -> Application - no
ReadService -> Adapter - no
Infrastructure -> Application - optionally, controlled
Contracts -> Projectionpolicy
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.