↑↓ to navigate Enter to open "…" all these words ANDOR to combine

Architecture governance

MMCA.Store: Architecture Scorecard

Canonical, version-controlled scorecard for this repo: the single source of truth for MMCA.Store's architecture scores. Scored against the rubric at ArchitectureEvaluationCriteria.md; framework-wide facts in ../MMCA.Common/FACTS.md. Remediation lives in RemediationBacklog.md; the cross-repo comparison in the workspace-internal Docs/Architecture/CrossRepoComparison.md (not published).

Evidence as of 2026-10-18 at MMCA.Common.* v1.244.0 (lockstep), git HEAD b40baf14 (clean tree). Rubric version 2.

At a glance

  • Maturity index = Σ(maturity×weight) ÷ Σ(weight×4) = 308 ÷ 316 = 97.5%
  • Implementation index = Σ(impl×weight) ÷ Σ(weight×10) = 657 ÷ 790 = 83.2%
  • Weaker axis: Implementation (execution quality), by ~14 points, a wider gap than ADC's.
  • Below maturity 4: 3 categories (§10 Messaging, §12 Performance, §21 Accessibility); see the maturity band.
  • Implementation band (implementation <= 8): 22 categories; see the implementation band.

MMCA.Store is a .NET 10.0 DDD/Clean Architecture e-commerce system (Catalog, Sales, Identity modules; Stripe checkout) running as independently hosted services behind a YARP gateway, database-per-service (Store_Catalog/Store_Sales/Store_Identity, each with its own outbox), collaborating via Result-over-the-wire gRPC and MassTransit integration events, and consuming MMCA.Common.* in lockstep with MMCA.ADC. Its strongest ground is inherited framework substance guarded by 62 architecture fitness-test classes, reference front-end security (section 26), and an operational floor of two-phase Bicep IaC, OIDC + managed identity, signed service tokens on the internal gRPC edge that returns customer data, a post-deploy smoke gate, a drilled DR restore and freshness-gated deploys. The weight of the remaining gap sits on the implementation axis: security (section 11) is the one weight-3 category at I7, and 18 categories are at M4/I8, led by the weight-3 SOLID constructor ceilings (section 1), the accepted lockstep deploy (section 7), UI architecture (section 18), state management (section 19) and front-end testing (section 28). Several of those I8 holds are recorded decisions rather than scheduled work (see Deliberate and accepted in the backlog).

Top 5 risks

  1. The Stripe webhook signing secret sits in plaintext in the shared cache: section 11 (impl 7, weight 3): the signing secret is cached in plaintext in the shared Redis with a 365-day lifetime (SharedCacheStripeWebhookSecretStore.cs:79, lifetime at :54; the registration service never prints it, StripeWebhookRegistrationService.cs:200-202). Two narrower red flags sit on the same row as recorded decisions: one shared identity is db_owner in all three databases (infra/SQL-MANAGED-IDENTITY.md:80) and every per-app Service Bus SAS rule carries Manage rights (infra/main.bicep:1069,1081,1093, rationale :1052-1057), both accepted at this scale. The internal Customers gRPC edge requires a user or service token (Identity.Service/Program.cs:343).
    • Remediation: protect the cached signing secret (encrypt it at rest in the cache or keep it out of the shared cache), and correct the stale comment that says every service connects with the SQL administrator login (infra/main.bicep:869-870). Expected: section 11 impl 7 holds while the accepted identity and SAS caps stand; the secret fix removes the one unaccepted red flag.
  2. Broker failures have no operating procedure: section 10 (mat 3, impl 8, weight 3): the only messaging alert matches the outbox's "dead-lettered" text (infra/main.bicep:462), while a consumer that exhausts its retries logs "moved to the error queue" (Common FaultIntegrationEventConsumer.cs:57), which no Store alert matches; infra/OPERATIONS.md has an outbox-row replay (:166) but no error-queue section, and its consumer inventory names one live flow where five consumers run (OPERATIONS.md:164).
    • Remediation: an alert-as-code on the error-queue log line or the broker metric, an OPERATIONS.md runbook with a replay-or-discard procedure, and a fitness rule requiring [EventName] on every event (Common-side). Expected: section 10 mat 3 to 4.
  3. Accessibility maturity is capped pending a human pass: section 21 (mat 3, weight 3): the 32-scan axe suite and the cart-drawer keyboard test (CartDrawerKeyboardTests.cs:21) gate the deploy (impl 8), but the rubric pairs axe-in-CI with a recorded manual screen-reader pass and the ACCESSIBILITY-SCREENREADER-PASS.md results log holds only the placeholder row (store-ACCESSIBILITY-SCREENREADER-PASS.md:70), matching ADC's M3.
    • Remediation: record a dated NVDA/VoiceOver pass in the runbook against the running app (needs a human; cannot be done headless). Expected: section 21 mat 3 to 4.
  4. Load evidence is capacity-planning cadence, not a merge gate: section 12 (mat 3): the k6 suite is real with pass/fail thresholds and a load-freshness deploy gate, but the test itself runs monthly/on-demand (load-test.yml:21 cron, workflow_dispatch at :12), so a latency regression can merge and deploy inside the 35-day freshness window. load-freshness (deploy.yml:807) runs the shared freshness-gate action in success mode, keyed on the latest successful run (:810-816), and carries a break-glass skip (:818-819).
    • Remediation: none scheduled: the monthly k6 run plus load-freshness is the accepted posture, because an in-band k6 run on every deploy would load production at a peak of about 67 users (Deliberate and accepted in the backlog). Expected: section 12 stays M3.
  5. The post-deploy rollback covers only a failed activation: section 17 (impl 8, weight 2) with section 7 (impl 8, weight 3): the deploy is one azure/arm-deploy of the whole main.bicep (deploy.yml:1317) with all five images pinned to one IMAGE_SHA (:1145), the accepted lockstep posture for a five-app fleet. Its rollback block (:1481-1527, app list :1363) skips every app whose newest revision is live (:1497), so a failure caught only by the HTTP probes rolls nothing back while the step reports "All apps rolled back" (:1525), and no Bicep build, lint or what-if runs before the deploy.
    • Remediation: make the rollback summary report what it actually did and align its comment (:1487-1488), then add a Bicep what-if before merge. Expected: section 17 impl 8 to 9.

Top 5 strengths

  1. Clean Architecture + DDD + CQRS depth, fitness-enforced: §3/§4 (impl 9): inherited framework substance, guarded by the shared NetArchTest suite (62 test classes, StoreArchitectureMap.cs:14-43).
  2. Full microservices parity on a race-safe data layer, §7 (impl 8) / §8 (impl 9): database-per-service + per-service outbox + versioned gRPC + MassTransit broker + RS256/JWKS, extractable modules (infra/main.bicep:971-976 per-service DBs, :1022 Service Bus), with the oversell race closed by an atomic conditional UPDATE under deterministic lock ordering plus a schema CHECK backstop (InventoryAllocationService.cs:71-73, InventoryItemConfiguration.cs:57-59) and destructive migrations blocked at the merge gate (deploy.yml:431-482).
  3. Observability and test strategy, gate-backed: §13 (impl 9) / §14 (impl 9): ObservabilityConventionTests.cs:7 pairs every SLO alert with an infra/OPERATIONS.md section and fails below four specs (:13), and the required build-and-test check holds a self-filtered line floor of 51.6 and an Application-layer branch floor of 72.8 (deploy.yml:374, :407) over a real-SQL integration tier.
  4. Reference front-end security, §26 (impl 9): in-memory access token + HttpOnly refresh cookie (no localStorage), hardened origin-pinned CSP, runtime-config fetch (ServerTokenStorageService.cs:9-12, BlazorCspPolicyProvider.cs:105).
  5. Drilled DR + real erasure path (§29 impl 9 / §30 impl 8): recorded 28.9-min restore (DISASTER-RECOVERY.md:209, drill-history table :207-212), the dr-freshness deploy gate + weekly cron + CI-gated GracefulShutdownTests, IAnonymizable anonymize-in-place + export + residency/PII fitness functions.

Scorecard

# Category Weight Maturity (0-4) Impl (0-10) Weighted (M·w / I·w) Evidence / Notes
1 SOLID Principles 3 4 8 12/24 Narrow-interface ctor DI, OCP via state strategy (no type-switch); the constructor-dependency ceilings are machine-enforced by ConstructorDependencyCountTests (services 6 at :35, controllers 8 at :53, handlers 7 at :71) in the merge-gating architecture tier (MMCA.Store.CI.slnf:53), Liskov substitution is guarded by SubstitutabilityTests (sealed subclass of the shared base over the Store module assemblies, no exclusions, Governance/SubstitutabilityTests.cs:14), and TimeProvider is injected over ambient DateTime.UtcNow, with one reviewed exception pinned by ClockReadTests.cs:18 (Order.cs:522). Held at 8, not 9: the rubric's 8+-dependency red flag (ArchitectureEvaluationCriteria.md:134) is present in four classes. AuthenticationService holds the service high-water mark with 6 constructor dependencies (AuthenticationService.cs:26-32, service ceiling 6 at ConstructorDependencyCountTests.cs:35) and three controllers sit at 8 (ShoppingCartsController.cs:51, ReviewsController.cs:55, ProductVariantsController.cs:37). Each ceiling is a recorded, deliberate one (Deliberate and accepted #1 in the backlog), and that accepted ceiling is what caps this row at 8. Two minor gaps sit beside it: an is-type dispatch on the Stripe SDK object in the payment adapter (StripePaymentService.cs:360) and a string switch on the webhook event type (ProcessPaymentWebhookHandler.cs:41). Evidence: Tests/Architecture/MMCA.Store.Architecture.Tests/Cqrs/ConstructorDependencyCountTests.cs:35,53,71; AuthenticationService.cs:26; Order.cs:149-158 (FrozenDictionary<OrderStatus,IOrderState>)
2 Design Patterns 2 4 9 8/18 Factory→Result, payment State machine, Saga (Stripe compensation), Specification, Decorator, Outbox, Repository/UoW: idiomatic, named. Evidence: Order.cs:191,149-158 (factory at :191, state table at :149-158); Orders/Saga/OrderCancelledSagaHandler.cs; Orders/Specifications/OrdersByCustomerSpecification.cs
3 Clean Architecture 3 4 9 12/27 NetArchTest-enforced layer rules; domain framework-pure. (The compile-time layer-guard MSBuild target is MMCA.Common-internal; it is not a Store-side gate.) Evidence: StoreArchitectureMap.cs:14-43 + LayerDependencyTests/DomainPurityTests; Order.cs:1-13 (imports only Common.Domain/Shared)
4 Domain-Driven Design 3 4 9 12/27 Aggregate root, Money VO, by-id cross-aggregate refs, domain events, invariants, factory→Result, rich state behavior; identifier aliases. Evidence: Order.cs (Money :58, OrderInvariants, OrderIdentifierType)
5 Vertical Slice Architecture 2 4 8 8/16 Cohesive command+handler+request+validator+mapper per operation; a deliberate layered-by-project hybrid (cross-cutting in the pipeline). Slice cohesion is machine-enforced pre-merge by SliceCohesionTests (sealed subclass of the shared SliceCohesionTestsBase, two real rule facts; the base carries no minimum-scanned-types floor, and the scan is non-vacuous in practice because StoreArchitectureMap anchors real Application assemblies) in the deploy-gating MMCA.Store.CI.slnf, the same gate ADC credits at M4/I8. The hybrid is an implementation-axis cap, not a maturity deduction. Held at 8, not 9: horizontal technical folders persist inside the module Application layers, gate-invisible where the validated type is cross-assembly (the co-location rule exempts them, MMCA.Common .../ArchitectureRules.Slices.cs:60; instance: Identity.Application/Users/Validation/ChangePasswordRequestValidator.cs:10 over the Shared ChangePasswordRequest while its command+handler live in Users/UseCases/ChangePassword/), and generic-CRUD operations dispatch to shared framework handlers (Catalog.Application/DependencyInjection.cs:101, Identity.Application/DependencyInjection.cs:78) with reads served by the generic IEntityQueryService (CategoriesController.cs:37). Evidence: Tests/Architecture/MMCA.Store.Architecture.Tests/Cqrs/SliceCohesionTests.cs:9; MMCA.Store.CI.slnf:53; Application/Orders/UseCases/Cancel/{CancelOrderCommand,CancelOrderHandler}.cs
6 CQRS & Event-Driven 2 4 8 8/16 Command/query split, outbox persist-then-publish, idempotent inbox (AddInboxMessages), Stripe saga; one cross-service event flow (ProductVariantChanged) + a deliberate in-process UserRegistered domain event; the outbox-to-broker-to-consumer round-trip is covered by the nightly MMCA.Store.CrossService.IntegrationTests (Testcontainers RabbitMQ+SQL). The ProductVariantChanged Added path is transactional: AddVariantCommand is ITransactional (AddVariantCommand.cs:25) and the publish is a durable ADR-114 InternalCommands row enrolled in the same transaction (AddVariantHandler.cs:79-82,92-97). Held at 8: a refused schedule is logged at Error and not retried (AddVariantHandler.cs:98-100). Evidence: Sales.Service/Program.cs:261; Contracts/IntegrationEventContractTests.cs; Tests/Integration/MMCA.Store.CrossService.IntegrationTests/
7 Microservices Readiness 3 4 8 12/24 Database-per-service + per-source outbox, async events + versioned gRPC, Polly resilience, extractable modules, 5 independently-built images. All per-service DBs share one physical SQL server, self-documented at infra/main.bicep:938. Maturity 4: the §7 fitness classes (MicroserviceExtractionTests / ModuleIsolationTests / ServiceContractPurityTests / ProtoContractTests; MicroserviceExtractionTests.cs:3) ship in MMCA.Store.CI.slnf:53 and execute in the required build-and-test job (deploy.yml:255, test run at :338). Held at 8, not 9: "services build/test/deploy independently" is half met. The BUILD half is per-image incremental (per-image dirty flags in the changes job deploy.yml:80-218, matrix :956, unchanged legs re-tagged instead of rebuilt :1031-1046); the DEPLOY half is a single azure/arm-deploy of the whole main.bicep (:1317) with all five container-app images pinned to one IMAGE_SHA (:1145), so every service takes a new revision on every deploy and a smoke failure that fails an activation rolls all five back together (rollback block :1481-1527, app list :1363). That is the rubric's "services that must deploy together" red flag, which keeps the row in the Strong band rather than Exemplary; the lockstep deploy is the recorded posture for a five-app, one-developer fleet (Deliberate and accepted in the backlog), so it caps the row the way the constructor ceilings cap section 1. A secondary rubric gap, not a downgrade basis: the Anti-Corruption Layer / Strangler Fig vocabulary has zero occurrences in the repo. Evidence: infra/main.bicep:966-999 (per-service DBs, names :971-973) + :1022 (Service Bus namespace); Sales.Service/Program.cs:246-247 (gRPC typed clients), :257 (AddBrokerMessaging); deploy.yml:825-843 (the cross-service-freshness step of the freshness job requires BOTH the RabbitMQ round-trip and the Service Bus emulator smoke to have succeeded in the same nightly run)
8 Data Architecture 3 4 9 12/27 Per-aggregate tx, soft-delete + filtered indexes, central audit (Common), RowVersion concurrency, versioned per-service migrations with a model-drift CI gate, LTR backups. The write path is race-safe by construction: stock decrements are an atomic conditional UPDATE (SET qty = qty - n WHERE qty >= n) with deterministic variant-id lock ordering, enlisted in the ambient transaction and stamping the audit columns explicitly because ExecuteUpdate bypasses the audit interceptor (InventoryAllocationService.cs:71-73, lock ordering at :60); a CK_InventoryItem_AvailableQuantity_NonNegative CHECK constraint backstops it at the schema so no future path can drive stock negative (InventoryItemConfiguration.cs:57-59); the checkout write phase (decrements + order insert + cart transition) is one ExecuteInTransactionAsync with the cross-service gRPC price fetch deliberately outside it, so remote latency never extends lock hold time (CheckOutHandler.cs:80); and an expand/contract guard fails any PR whose new migration Up() drops a column/table/index without an EXPAND-CONTRACT-OVERRIDE marker, failing closed when the base diff is unresolvable (deploy.yml:431-482; fail-closed branch at :460, override marker check at :474-476; policy at CONTRIBUTING.md:56, CI enforcement described at :68). Both migration gates sit in the required build-and-test check. Held at 9, not 10: Identity carries IConcurrencyAware mutation requests but has no concurrency round-trip test, so the stale-version proof (412 Precondition Failed on a stale If-Match, 428 Precondition Required when the write names no version) covers 2 of 3 modules; the shared ConcurrencyConventionTests rule scans Application types named *UpdateRequest, of which Store has none, so that particular gate is vacuous here (the substance stands without it). Evidence: InventoryAllocationService.cs:71-73; InventoryItemConfiguration.cs:57-59; CheckOutHandler.cs:80; API-level round-trip proof in Tests/Integration/MMCA.Store.Catalog.IntegrationTests/Concurrency/StaleRowVersionConflictTests.cs:19 (412), :41 (428), :56 (the child-entity token path, 412 on a stale variant version) and Tests/Integration/MMCA.Store.Sales.IntegrationTests/Concurrency/OrderTransitionConcurrencyTests.cs:25 (412), :64 (428); raw-IQueryable ban with an EMPTY allowlist in Tests/Architecture/MMCA.Store.Architecture.Tests/Cqrs/RawQueryableConventionTests.cs:14 (in MMCA.Store.CI.slnf:53); per-module model-drift gate deploy.yml:415; main.bicep:974-985 (LTR policy)
9 API & Contract Design 2 4 9 8/18 RFC 9457 Problem Details, header versioning, pagination, DTO decoupling (ADR-001), gRPC .proto, OpenAPI served non-prod, and a demonstrated v2 contract: ServiceInfoController carries [ApiVersion("1.0", Deprecated)] + [ApiVersion("2.0")]. The guards are seven contract-guard files across all three services (OpenAPI shape/path-floor + RFC 9457 Problem Details for Catalog/Sales/Identity, incl. the Store-specific 409 stale-RowVersion probe, plus ServiceInfo API-versioning on Catalog), running in integration-tests, which is PR-only (deploy.yml:589-595, the pull_request condition at :595, absent from deploy.needs at :1084) but a server-side REQUIRED status check on main with strict=true (CONTRIBUTING.md:85), and merging to main is the prod deploy, so nothing ships without them green; a committed build-time OpenAPI baseline for the three services is diffed inside the required build-and-test job (deploy.yml:291-307); ProtoContractTests additionally freezes the full cross-service gRPC wire contract in the merge-gating architecture tier (ProtoContractTests.cs:9), and IntegrationEventContractTests freezes the .v1 event wire names (IntegrationEventContractTests.cs:9), documented in a wire-name and field table (store-Specification.md:963). Below 10: OpenAPI not exposed in prod (internal behind gateway); the OpenAPI gate detects changes but does not classify breaking ones; the Catalog OpenApiContractTests is a path-count floor plus three pinned paths (OpenApiContractTests.cs:17); the async-contract table is hand-maintained, with no AsyncAPI artifact; v2 demonstrated on one anonymous diagnostic endpoint in one service (the other 21 controllers are v1.0-only). Evidence: Catalog.API/Controllers/ServiceInfoController.cs:18; Tests/Integration/MMCA.Store.Catalog.IntegrationTests/Contract/{ApiVersioningTests,OpenApiContractTests,ProblemDetailsContractTests}.cs + Sales/Identity siblings; OrdersController.cs:51,88,117,124-126; Sales.Service/Program.cs:168 (AddOpenApi), :316 (non-prod MapCommonOpenApi)
10 Messaging & Integration Architecture 3 3 8 9/24 Rubric v2 category (ADR-110) at the v2 default weight 3. Broker topology and transport are ADR-governed (ADR-066: RabbitMQ locally, Service Bus in production, Sales.Service/Program.cs:257 AddBrokerMessaging); five consumers run (Sales: ProductVariantChanged, ProductInfoChanged, CustomerErased at Sales.Service/Program.cs:261; Catalog: OrderFulfilled, CustomerErased at Catalog.Service/Program.cs:274), with the inbox on in all three services (Sales.Service/appsettings.json:58) and redelivery idempotency tested (CustomerErasedHandlerTests.cs:111). Poison-message handling follows ADR-087, with per-service outbox/inbox retention indexes (MMCA.Store.Migrations.SqlServer.Sales/Migrations/20260725133726_AddOutboxInboxRetentionIndexes.cs) and a 30-day outbox dead-letter retention setting (infra/main.bicep:1526); saga compensation on order cancellation (Orders/Saga/OrderCancelledSagaHandler.cs); the gateway kept to routing and auth only per ADR-088 with RouteMapTests pinning every route; the wire contract of all four integration events frozen by Tests/Architecture/MMCA.Store.Architecture.Tests/Contracts/IntegrationEventContractTests.cs:11 in the merge-gating architecture tier (MMCA.Store.CI.slnf:53); and the real broker round-trip enforced as a deploy gate: cross-service-freshness (deploy.yml:826, in deploy.needs at :1084, required at :1114) requires both the RabbitMQ and the Service Bus emulator tiers of cross-service-tests.yml:141 to have succeeded in one recent run (deploy.yml:841-844). The IsFailure-guarded cache-eviction pattern ADR-001 describes is present (Catalog.API/Controllers/ProductsController.cs:162,165). Maturity 3, not 4: the rubric requires a dead-letter path with an operational procedure (ArchitectureEvaluationCriteria.md:336), and the broker error queue has none. The only messaging alert matches the outbox text "dead-lettered" (infra/main.bicep:462), while a consumer that exhausts its retries logs "moved to the error queue" (Common FaultIntegrationEventConsumer.cs:57), which no Store alert matches; ADR-087 records no alert, runbook or replay path for that queue (087-broker-poison-message-handling.md:202); and nothing requires [EventName] on every event (all four carry it by convention). These are the same levers that hold ADC's §10 at M3. Held at 8, not 9, on the same gap plus three Store-specific residuals: the outbox dead-letter runbook names one live consumer flow where five run (infra/OPERATIONS.md:164), CustomerErased and ProductInfoChanged have no real-broker round-trip test, and no ordering expectation is declared for ProductVariantChanged (IntegrationEventContractTests.cs:12). Evidence: infra/main.bicep:462; deploy.yml:826,1084; IntegrationEventContractTests.cs:11
11 Security 3 4 7 12/21 RS256/JWKS (no shared secret), server-side resource authz (404-not-403) with class-level [Authorize], ownership specifications and HasPermission (OrdersController.cs:52,140,188), Key Vault secrets read through the UAMI (infra/main.bicep:1481-1486), EF parameterized, a checkout redirect allow-list that fails closed (CreateCheckoutSessionCommandValidator.cs:73). Maturity 4: the build fails on any controller or page without an explicit authorization decision (AnonymousEndpointTests.cs:86), the supply-chain audit is in deploy.needs (deploy.yml:1084), and NuGetAudit runs in mode all (Directory.Build.props:45-46). The internal Customers gRPC edge, which returns email and full name by raw customer id, requires a user JWT or a signed service token: Identity registers AddServiceTokenAuthentication (Identity.Service/Program.cs:173) and maps CustomersGrpcService with .RequireAuthorization(ServiceTokenDefaults.InternalCallerPolicy) (:343), Sales mints the token through AddServiceTokenClient (Sales.Service/Program.cs:289) and the Customers client opts in with WithServiceToken (Identity.Contracts/DependencyInjection.cs:46), the signing key is a required deploy parameter stored as the Key Vault secret Grpc--ServiceToken--SigningKey (infra/main.bicep:38, :1366-1369, env refs :1604, :1871) and the deploy fails fast when GRPC_SERVICE_TOKEN_SIGNING_KEY is unset (deploy.yml:1151, :1226-1230), and CustomersGrpcAuthorizationTests.cs:40 pins the refusal; the Sales export edge also requires authorization (Sales.Service/Program.cs:325). Catalog's ProductVariantsGrpcService is anonymous and carries no PII, a choice its host comment records (Catalog.Service/Program.cs:340-345). Held at 7, not 8, on three narrower red flags: the Stripe webhook signing secret is cached in plaintext in the shared Redis for 365 days (SharedCacheStripeWebhookSecretStore.cs:79, lifetime :54; the registration service never prints it, StripeWebhookRegistrationService.cs:200-202), one shared identity is db_owner in all three databases (infra/SQL-MANAGED-IDENTITY.md:80), and every per-app Service Bus SAS rule carries Manage rights (infra/main.bicep:1069,1081,1093, kept because MassTransit creates the topology at runtime, :1052-1057). The shared identity and the Manage rights are accepted at this scale (Deliberate and accepted in the backlog), so the signing-secret cache is the one unaccepted red flag. Internal ingress allows insecure transport with no peer-traffic encryption configured (infra/main.bicep:1476), and the SQL server keeps admin password auth with public network access enabled (infra/main.bicep:835). Evidence: Identity.Service/Program.cs:173,343; Sales.Service/Program.cs:289; AnonymousEndpointTests.cs:86; infra/main.bicep:1069,1366; SharedCacheStripeWebhookSecretStore.cs:79
12 Performance & Scalability 2 3 8 6/16 Async throughout, projections/AsNoTracking/paging on hot paths, tiered cache (output + Redis), stateless scale-out, a real k6 load test, client Web Vitals measured per E2E run (LCP/CLS/TTFB/FCP written as CI artifacts), one batched cross-service existence check in bulk inventory (Sales.Application/Inventory/UseCases/BulkSet/BulkSetInventoryHandler.cs:36-39, rationale :31-35), cross-replica output-cache eviction for non-controller writes (Catalog.Service/Program.cs:177, broker half :290), and bounded broker backpressure per service: each MessageBus section sets PrefetchCount 8 and ConcurrentMessageLimit 4, sized to Store's Basic-tier SQL connection budget rather than copied from ADC (Catalog.Service/appsettings.json:63-64, Sales.Service/appsettings.json:67-68, Identity.Service/appsettings.json:57-58, each with the section 12 comment above it), pinned by Tests/Architecture/MMCA.Store.Architecture.Tests/Governance/MessageBusBackpressureTests.cs:10 (binds MessageBusSettings from all three embedded files and asserts both knobs positive and prefetch covering concurrency; the binding and assertions live in the Common base class it subclasses) in the merge-gating architecture tier (Store commit d1cae3ed). Maturity 3, not 4: no perf check runs on the merge path, and that is the recorded posture (the monthly k6 run plus load-freshness; an in-band k6 run on every deploy is Deliberate and accepted in the backlog). load-test.yml is a monthly cron (:21) plus workflow_dispatch (:12); the only deploy-chain hook, load-freshness (deploy.yml:807), is push-only (:779), bounds staleness rather than detecting regressions, runs the shared freshness-gate action in success mode over a 35-day window keyed on the latest SUCCESSFUL k6 run (:810-816) so a threshold failure blocks nothing until the last success ages out, and carries a break-glass skip with a justification (:818-819), which the deploy reads as success (:1084 needs, :1114); the Web Vitals budgets ride an e2e-gate the deploy accepts as skipped (:1117); none of the four required merge checks is a latency gate (CONTRIBUTING.md:85). Held at 8: the k6 asset is anonymous, read-only and Catalog-only at 40 VUs (Tests/Load/k6/catalog-read-load.js:3,15). Evidence: Tests/Load/k6/catalog-read-load.js + load-test.yml:12,21 (synthetic-traffic header :7-8, key wired at :86); Tests/E2E/MMCA.Store.E2E.Tests/Workflows/WebVitalsTests.cs + e2e.yml:383 (WEB_VITALS_OUTPUT_DIR); OrdersController.cs:125-155 (bounded pageSize :133, asTracking: false :147, field projection :144)
13 Observability & Operability 2 4 9 8/18 OTel logs/traces/RED metrics via ServiceDefaults (incl. MMCA.Common.Outbox), /health+/alive+/health/ready, App Insights + 5 SLO alert specs + action group + saved SLO workbook provisioned in IaC, per-alert operations runbook in-repo, correlation, poll-span noise control, CI-gated graceful shutdown. Maturity 4: the alert-to-runbook pairing is a CI-gated fitness function: ObservabilityConventionTests parses the embedded infra/main.bicep sloAlertSpecs and fails the merge gate on any alert without a severity-correct OPERATIONS.md section (5-spec non-vacuity floor, MinimumAlertSpecs => 5 at ObservabilityConventionTests.cs:13; corrected 2026-10-19). Evidence: Tests/Architecture/MMCA.Store.Architecture.Tests/Governance/ObservabilityConventionTests.cs (a sealed subclass; the four [Fact]s live in MMCA.Common.Testing.Architecture/Bases/Governance/ObservabilityConventionTestsBase.cs); MMCA.Store.CI.slnf:53; main.bicep:330 (sloAlertSpecs), :820 (workbook); infra/OPERATIONS.md:15; Tests/Hosts/MMCA.Store.Gateway.Tests/GracefulShutdownTests.cs (a sealed subclass; the assertion body lives in MMCA.Common.Testing/GracefulShutdownTestsBase.cs)
14 Testability & Test Strategy 3 4 9 12/27 Non-inverted pyramid + arch fitness + integration tier gating deploy; the unit-tier line floor is 51.6 measured on Store's own code (reportgenerator +MMCA.Store.*;-*.Tests;-MMCA.Store.*.Contracts, ~54% actual per the step comment), and a second, narrower Application-layer BRANCH floor runs on the same cobertura file in the same build-and-test job: 72.8 over the three module *.Application assemblies (+MMCA.Store.*.Application;-*.Tests), 2 points under the measured 74.8%, with a guard that fails the step when fewer than 3 assemblies are covered so the floor cannot pass vacuously (Store commit 89692c78), the gate shape ADC runs. A nightly real-broker round-trip tier (MMCA.Store.CrossService.IntegrationTests, Testcontainers RabbitMQ+SQL, cross-service-tests.yml) covers the outbox-to-broker-to-consumer flow the in-process tests only approximate. Held at 9 (not 10): the broker round-trip is non-gating nightly and the SQL integration suite runs only in CI. Evidence: deploy.yml:350-376 (line-floor step; floor 51.6 at :374, self-filter at :372), :378-409 (Application-layer branch-floor step; filter at :398, 3-assembly guard at :401-404, floor 72.8 at :407, measured baseline recorded at :388), :325-338 (the MMCA.Store.CI.slnf test run under coverage collection); arch tests; Common.Testing.E2E
15 Best Practices & Code Quality 2 4 8 8/16 Five analyzers at error + TWAE + CPM; targeted vuln pin; consistent Result pattern; zero TODO/HACK/FIXME in Source/, every hand-written pragma carries an inline reason. Suppression hygiene is documented: every global NoWarn code carries an inline reason (Directory.Build.props:16-30: CS1591 :16-20, EXTEXP0001 :21-25, S8970 :26-29, global line :30), RMG020 sits in a documented .Application-only group (:60-68), the test-tier suppressions are centralized (:50-58) and no test csproj declares its own NoWarn; no advisory is accepted (NuGetAudit / NuGetAuditMode=all declared at :45-46, the accept-an-advisory procedure stated at :36-44). The weekly maui-audit.yml covers the MAUI head's vulnerability audit (§32 substance by its own header, :4). Held at 8, not 9, on criterion 1 alone: the MAUI head is in neither solution filter (MMCA.Store.CI.slnf:24-25, maui-audit.yml:17 and .editorconfig:842 all say so), so its analyzers/TWAE are review-only, an isolated gap in the 7-8 band. Evidence: Directory.Build.props:10-14,104-125; Directory.Packages.props:61-64,73 (the five analyzers), :111-112 (targeted OpenTelemetry.Api vuln pin); .editorconfig:312; deploy.yml:527 (the gating supply-chain job)
16 AI-Native Application Architecture 2 N/A N/A N/A Rubric v2 (ADR-110): N/A, no product feature calls a language model (the workspace's agentic engineering tooling is scored in §33); weight 2 leaves both denominators until a feature does. The Maintainability & Evolvability criteria this number carries under rubric v1 score under §34 (coupling, tech-debt register), §32 (lockstep upgrades) and §33 (onboarding). The lockstep invariant is executable: FrameworkVersionConsistencyTests asserts every MMCA.Common.* pin shares one version and fails the build on a partial sweep, in the CI merge gate. Evidence: Tests/Architecture/MMCA.Store.Architecture.Tests/Governance/FrameworkVersionConsistencyTests.cs; MMCA.Store.CI.slnf:53; Directory.Packages.props:8-107
17 DevOps & Deployment 2 4 8 8/16 CI gates (locked restore, analyzers-as-errors Release build, OpenAPI drift, tests, two coverage floors, EF model drift, expand/contract guard) in the required build-and-test check, then a deploy job that needs every gate (deploy.yml:1084) and accepts only e2e-gate as skipped (:1117); two-phase Bicep; OIDC, with the token permission granted only to the jobs that sign in to Azure (:1120); cost-guard, dr-drill and load-test workflows. MI-SQL is active in production (repo variable USE_MANAGED_IDENTITY_SQL=true, wired at :1170 and folded into the Stage 3 parameters at :1297-1313), so all three services authenticate passwordless through managed identity. The deploy path admits no zero-test production rollout: build-and-test runs the MMCA.Store.CI.slnf unit, architecture and bUnit tier on every PR (deploy.yml:255, PR condition :261, test run :338), and branch protection enforces admins with strict up-to-date required checks, so the merged tree is the PR-tested tree. Held at 8, not 9: the only environment is prod (ENVIRONMENT_NAME: prod at deploy.yml:41); no Bicep build, lint or what-if runs before azure/arm-deploy (:1315-1317), so an infra-only PR merges with the template unexecuted; SQL stays dual-auth, with the password admin configured (infra/main.bicep:831-832), SQL_ADMIN_PASSWORD passed on every deploy (deploy.yml:1146), and publicNetworkAccess: Enabled plus the AllowAzureServices rule (infra/main.bicep:835,858); and the post-deploy rollback covers only a failed activation. The smoke step (deploy.yml:1355) skips every app whose newest revision is live (:1497), so a failure caught only by the tier-2 HTTP probes rolls nothing back, while the step still prints "All apps rolled back" (:1525) and its comment says a healthy newest revision steps back one revision (:1487-1488); under activeRevisionsMode: 'Single' (infra/main.bicep:1469) the predecessor is already inactive, so the active-revision filter (deploy.yml:1504) finds no candidate. ADR-080 accepts single-revision mode and keeps the skip-live guard, and no rollback rehearsal is recorded. Smaller points: the Trivy image scan is report-only (deploy.yml:1072), and the apps' managed identity and its role grants are bootstrapped outside IaC (infra/DISASTER-RECOVERY.md:93). Evidence: deploy.yml:1084,1117 (gate chain), :1355 (smoke), :1363 (app list), :1481-1527 (rollback block); infra/SQL-MANAGED-IDENTITY.md; ADR-080
18 UI Architecture & Components 3 4 8 12/24 Code-behind split, @inherits DataGridListPageBase<ProductDTO>, scoped cart service owns data/behavior, reuse of Common.UI primitives. Maturity 4: the container/presentational conventions are machine-enforced pre-merge by UIArchitectureConventionTests (sealed subclass of the shared base, 400-line code-behind cap + 120-line inline @code cap, non-vacuous MinimumCodeBehindFiles guard; largest code-behind is 389 lines), running in the MMCA.Store.CI.slnf tier of the required PR check build-and-test, the same mechanism as ADC's M4. Held at 8, not 9: four code-behinds sit near the 400-line cap (ProductReviewsPanel.razor.cs 389 lines, 11 under the cap, CatalogBrowse.razor.cs 371, CustomerDetail.razor.cs 368, ShoppingCartDetail.razor.cs 360), and the cap is a size proxy rather than proof of container/presentational purity; the cap stays at 400 by recorded decision (lowering it to 300 is Deliberate and accepted in the backlog); the lever for 9 is not yet identified. Evidence: Tests/Architecture/MMCA.Store.Architecture.Tests/Ui/UIArchitectureConventionTests.cs:9; StoreArchitectureMap.cs:27,35,43; MMCA.Store.CI.slnf:53; deploy.yml:255,338; ProductList.razor.cs:17; CartDrawer.razor:49
19 State Management & Data Flow 3 4 8 12/24 Single source of truth, scoped (no static cross-user state), unidirectional flow with OnChange+InvokeAsync(StateHasChanged)+Dispose, single-flight token hydrate. Maturity 4: both §19 red flags are machine-enforced pre-merge by StateManagementConventionTests (mutable-static-state reflection scan over the Layer.Ui assemblies with a non-vacuous guard, plus the singleton-*StateService source scan), in the deploy-gating MMCA.Store.CI.slnf; CartStateService is registered TryAddScoped, proving the rule the gate enforces. Drawer visibility goes through the notify path: ICartStateService.cs:45 is getter-only with OpenDrawer/CloseDrawer/ToggleDrawer at :105-111, both setters notify at CartStateService.cs:332-342 behind a private set at :65, and CartDrawer.razor:3 binds one-way. The superseded-load generation guard covers all seven detail pages (OrderDetail.razor.cs:130, ProductDetail.razor.cs:67, CatalogProductDetail.razor.cs:70, ShoppingCartDetail.razor.cs:72, CategoryDetail.razor.cs:70, CustomerDetail.razor.cs:93, InventoryItemDetail.razor.cs:66) plus CatalogBrowse.razor.cs:189 and UserDetail.razor.cs:90,98 (the generation guard is the LoadGuard inherited from the Common page base). Held at 8, not 9: there is no client-side staleness or invalidation strategy; the IUiReadCache opt-in is withdrawn from all six UI entity services (commit e60bb8be) pending a per-service invalidation story (ShoppingCartService.cs:18 constructs EntityServiceBase with no read cache), so the rubric's cached-with-invalidation criterion is carried by the server output cache alone; the lever for 9 is not yet identified. The stale-IsDirty red flag is also test-checked: every UnsavedChangesGuard binds a live IsDirtyAccessor (ProductDetail.razor:12), which the shared FormsConventionTests base requires (Common FormsConventionTestsBase.cs:66). Evidence: Tests/Architecture/MMCA.Store.Architecture.Tests/Ui/StateManagementConventionTests.cs:10; Sales.UI/DependencyInjection.cs:34 (TryAddScoped); MMCA.Store.CI.slnf:53; CartStateService.cs:59 (the OnChange publisher); CartButton.razor:67,69 (InvokeAsync(StateHasChanged) + Dispose); Common MMCA.Common.UI.Web/Services/ServerTokenStorageService.cs:45-54 (the single-flight hydrate is framework code, not Store code)
20 Design System & UI Consistency 2 4 8 8/16 MudBlazor + Common.UI theme/tokens used consistently, shared grid-paging wrapper; the brand-color token convention is CI-enforced by BrandColorTokenTests (sealed subclass at Tests/Architecture/MMCA.Store.Architecture.Tests/Ui/BrandColorTokenTests.cs:10, the one StoreHome stylesheet both UI hosts share embedded into the test assembly at MMCA.Store.Architecture.Tests.csproj:10, in MMCA.Store.CI.slnf:53). No Style=/CellStyle= attribute remains under Source/**/*.razor: the values ride the shared semantic classes defined once in Source/Hosts/UI/MMCA.Store.UI.Web/wwwroot/store.css:28 (with a comment on why no !important is needed) and mirrored in the MAUI head's MMCA.Store.UI/wwwroot/app.css:70, Common's .detail-card, or MudBlazor's own d-flex/flex-column/flex-1 utilities; the list pages consume them (CategoryList.razor:24 .list-search-field, :95 CellClass="grid-cell-actions"; CustomerList.razor:82; ReviewList.razor:110), and a bUnit fact pins the sweep (Tests/Modules/Catalog/MMCA.Store.Catalog.UI.Tests/Pages/Categories/CategoryListTests.cs:127 asserts the action column renders td.grid-cell-actions), in MMCA.Store.CI.slnf:40, executed by the required build-and-test check (deploy.yml:325). Maturity 4: the token guard and the bUnit pin are both merge gates. Held at 8, not 9: the StoreHome landing stylesheet hard-codes a hex palette behind !important with only --mmca-primary tokenized (Catalog.UI/Pages/Home/StoreHome.razor.css:218,289; one stylesheet in Catalog.UI serves both hosts, the MAUI head's UI/Pages/StoreHome.razor:3 wrapping the shared page), the rubric's library-defaults-fought red flag in one isolated place, which is the 7-8 band's "minor, isolated gap" rather than reference quality. Evidence: App.razor:11; BrandColorTokenTests.cs:10; store.css:28; app.css:70; CategoryList.razor:24,95; CustomerList.razor:82; ReviewList.razor:110; CategoryListTests.cs:127; Catalog.UI/Pages/Home/StoreHome.razor.css:218,289
21 Accessibility (a11y) 3 3 8 9/24 Strong semantics/ARIA, stated WCAG 2.1 AA, 32 axe scans (23 ScanAsync + 9 ScanGridAsync) over the public, shopper, and Catalog/Sales/Identity admin surfaces plus home in both palettes (dark-palette home scan at AccessibilityTests.cs:495), pinned to real WCAG 2.1 AA tags (AxeOptions.cs:17-24), and the axe suite gates the deploy (the chromium e2e-gate in deploy.yml's needs; nightly keeps the full matrix). Maturity 3, not 4: the rubric pairs axe-in-CI with a recorded manual screen-reader pass, and the ACCESSIBILITY-SCREENREADER-PASS.md results log holds only the placeholder row (:70, header :68-69; the runbook's own :75-76 names a dated row as what lifts the deduction), matching ADC. The e2e-gate is also chromium-only, UI-change-conditioned, and a SKIPPED gate is accepted by deploy (chromium-only at deploy.yml:765, UI-scoped at :762, skipped-tolerance at :1117); the single engine and the UI-scoping are both recorded Actions-minute trade-offs (Deliberate and accepted in the backlog). Keyboard operability is automated on the checkout path: CartDrawerKeyboardTests (sealed subclass of the shared KeyboardOperabilityTestsBase, CartDrawerKeyboardTests.cs:21) checks the skip link and focus indicators on home and catalog plus the cart drawer's modal keyboard contract (Enter opens it and moves focus in, the focus trap holds Tab, Escape closes it and returns focus, :5-11). Held at 8, not 9: keyboard and focus-order coverage stops at that one surface, every grid scan disables aria-input-field-name for the MudBlazor pager combobox (accepted, AxeOptions.cs:35-46), and dark-palette contrast is scanned on one page (:495). Evidence: Tests/E2E/MMCA.Store.E2E.Tests/Workflows/AccessibilityTests.cs:30-623; store-ACCESSIBILITY-SCREENREADER-PASS.md:70 (Website docs-src/guides/; placeholder row only); CartDrawer.razor:10,40,45,96-122; deploy.yml:752-765 (e2e-gate job, rationale :754-761, UI-scoping :762, browsers: '["chromium"]' :765), :1084 (e2e-gate in deploy needs), :1117 (a SKIPPED gate is accepted)
22 Responsive & Cross-Browser 2 4 8 8/16 Fluid layouts on the design-system grid with xs/sm/md/lg breakpoints (CatalogBrowse.razor:83), uniform grid→mobile-card reflow via MobileInfiniteScrollList on all seven admin list pages (ProductList.razor:26-32 plus the Category, Review, Order, InventoryItem, ShoppingCart and Customer lists), and a defined, Playwright-verified chromium/firefox/webkit matrix (e2e.yml:139-142). Maturity 4: cross-engine coverage is CI-enforced by a cross-browser-freshness job (deploy.yml:857, rationale :849-856) that walks completed e2e.yml runs for the latest successful E2E (firefox) and E2E (webkit) jobs inside a 10-day window (window-days at :873), sits in deploy.needs (:1084) and is a hard condition of the deploy job (needs.freshness.result == 'success', :1114), the same mechanism that earns ADC its M4. Both engines held in-window green proofs at the evidence date (webkit run 35192784223, firefox run 35008122578). The per-deploy e2e-gate is chromium-only (:765, job :752) and the scheduled non-chromium legs are continue-on-error (e2e.yml:149; crons :46-47, Monday firefox :140, Thursday webkit :141), so the gate proves recency rather than that this exact tree passes on those engines, and a workflow_dispatch break-glass can skip it with a justification (deploy.yml:880-881, input declared at :26); both are priced caveats in the backlog (a TD under #22 and the Deliberate and accepted entry), the same treatment ADC's row carries, not a cap. Held at 8, not 9, on two partial criteria: density is hard-coded Dense="true" across the grids with no user-selectable comfortable/compact option (ProductList.razor:61, ReviewList.razor:57), and there is no coarse-pointer/touch-target CSS and no mobile-viewport E2E coverage (zero SetViewportSize hits under Tests/E2E), so the mobile layout is implemented but not automatically verified. Evidence: deploy.yml:857,873,880-881,1084,1114 (cross-browser-freshness), :752,762,765 (e2e-gate); e2e.yml:46-47,139-142,149; ProductList.razor:26-32,61; CatalogBrowse.razor:83
23 Front-End Performance 2 4 8 8/16 Server paging/debounce/@key/output-cache on admin grids; the public CatalogBrowse uses server-side paging (GetPagedAsync + MudPagination) and cart enrichment resolves names via the targeted by-variant-id batch lookup. Maturity 4: the Core Web Vitals budgets are HARD assertions (WebVitalsTests.cs:58-61 measures against a WebVitalsBudget and :71 asserts within it; the per-metric assertion body lives in Common MMCA.Common.Testing.E2E/Infrastructure/WebVitalsCollector.cs:137, no soft mode) running unfiltered inside the deploy-gating chromium e2e-gate (deploy.yml:1084 needs), the evidence shape ADC credits at M4; this category's enforcement is per-deploy and independent of §12's k6 cadence. Evidence: Tests/E2E/MMCA.Store.E2E.Tests/Workflows/WebVitalsTests.cs:58-61,71; deploy.yml:752-765,1084; CatalogBrowse.razor.cs; CartStateService.cs
24 Forms, Validation & UX Safety 2 4 8 8/16 Unsaved-changes guard with current-state accessor (9 pages), double-submit blocked, all states designed, destructive confirm, abandoned-payment recovery. Maturity 4: the four admin create forms' guard/dirty/validated-MudForm/Required markers are machine-enforced by the CI-gated FormsConventionTests (MinimumCreateForms=4), matching ADC on the same evidence; the four create forms source their field rules from a DataAnnotations form model through Common's ModelValidation bridge. Held at 8: client validation is DataAnnotations-level with per-field parity on the email and password rules, not full FluentValidation parity with the server rule set, and no executable check ties the client rules to the server rules; full client parity is not pursued because it would duplicate the server rule set (Deliberate and accepted in the backlog). Evidence: Tests/Architecture/MMCA.Store.Architecture.Tests/Ui/FormsConventionTests.cs; MMCA.Store.CI.slnf:53; ProductCreate.razor:12,35,39; CartStateService.cs:257 (checkout idempotency key against double-submit)
25 Navigation & Information Arch 2 4 8 8/16 Bookmarkable typed route constants (CatalogRoutePaths), server-enforced [Authorize]/AuthorizeView (not UI-only) behind a fallback policy, 404 and 403 handled in the shell (Common Routes.razor:14,49), breadcrumbs on detail and create pages, catalog search state kept in the URL (CatalogBrowse.razor.cs:44-53), and an SSR session cookie so deep links and F5 do not bounce. Maturity 4: role-guard regression is CI-gated by the three sealed per-module *RouteAuthorizationTests, which govern all 19 Admin-gated pages (CatalogRouteAuthorizationTests.cs:29 floor 3, SalesRouteAuthorizationTests.cs:32 floor 2 plus the shared-/orders not-admin-gated guard at :35, and Identity listing .Pages.Customers, .Pages.Roles, .Pages.Users.UserList and .Pages.Users.UserDetail at IdentityRouteAuthorizationTests.cs:26-32); AnonymousEndpointTests.cs:86 requires every routable page to declare its authorization decision and FallbackAuthorizationTests.cs:88 pins the host fallback policy; all run in the required build-and-test check (deploy.yml:338). The framework notification pages are hidden because Store opts in to HideNotificationPagesWhenUnregistered (UI.Web/appsettings.json:18, UI.Web.Client/wwwroot/appsettings.json:16); the MAUI head does not set it. Doc drift scores on the implementation axis, not maturity. Held at 8, not 9: store-NavigationFlow.md matches the shell (/users route-tested at :15, /orders/{Id} [AllowAnonymous] at :66 and :225, matching OrderDetail.razor:11, the notification pages hidden at :236) and carries a Route Contract table (:188), but no Store test reads it, so drift between the doc and the @page routes goes uncaught; route parameters are untyped (/products/{Id} binds a string at ProductDetail.razor.cs:28, the same on every detail page; corrected 2026-10-19: all eight detail routes are now typed {Id:int} with an int Id parameter, Store #214, score not re-assessed); and the Roles menu item is shown by capability (IdentityUIModule.cs:29-35) while /roles requires the Admin role (RoleList.razor:2), so a non-Admin role granted roles:manage at runtime sees a menu item that answers 403. The Identity route test keeps the shared base floor of 1 while governing 7 pages (Common RouteAuthorizationTestsBase.cs:47). Lever for 9: a Store NavigationContractTests that reads the Route Contract in the published store-NavigationFlow.md (:188) and compares it with every @page route and its authorization attributes (the ADC pattern). Evidence: ProductList.razor:4; Tests/Modules/{Catalog,Sales,Identity}/MMCA.Store.*.UI.Tests/*RouteAuthorizationTests.cs in MMCA.Store.CI.slnf:40,46,52; UI.Web/Program.cs:280 (Common.UI assembly routed)
26 Front-End Security 3 4 9 12/27 Access token in-memory, refresh token in HttpOnly cookie (no localStorage), hardened origin-pinned CSP (no unsafe-inline script-src in prod, frame-ancestors none), runtime /client-config fetch (no secrets in bundle), UseAuthenticatedNoStore. Held at 9 on the basis recorded in ADC's §26 row: the CSP comes from the same Common provider, whose deliberately permissive style-src 'unsafe-inline' and img-src https: directives sit at BlazorCspPolicyProvider.cs:106-107. Evidence (the implementation lives in Common and is consumed byte-identically here): Common MMCA.Common.UI.Web/Services/ServerTokenStorageService.cs:9-12; Common MMCA.Common.UI/Services/Auth/WasmTokenStorageService.cs; Common MMCA.Common.UI.Web/Security/BlazorCspPolicyProvider.cs:105 (script-src, 'unsafe-inline' in Development only), :113 (frame-ancestors 'none')
27 Internationalization (i18n) 1 4 8 4/8 Full ADR-027 adoption (supersedes ADR-011): en-US + es .resx pairs across all three module UIs, the API error resources, both StoreHome hosts, and the three UI modules' nav items (30 base / 30 .es siblings), UseCommonRequestLocalization (UI.Web/Program.cs:233, registering both supported cultures and UI cultures, MMCA.Common .../WebApplicationExtensions.cs:88-89) + MapCultureEndpoint (:102), per-module AddErrorResources (Identity.Service/Program.cs:223), User.PreferredCulture + AddUserPreferences migration. No hard-coded literal ships: snackbars use whole-sentence page keys (including the cart/checkout/Stripe strings; raw {ex.Message} never surfaces), breadcrumb labels localize from Breadcrumb.* keys built in OnInitialized, nav menus localize via NavItem.TitleResource, and MudBlazor built-in chrome localizes via the framework's ResxMudLocalizer. Money formatting is culture-aware: MoneyExtensions.cs resolves the symbol from the price's own currency (MMCA.Common .../MMCA.Common.UI/Extensions/MoneyExtensions.cs:18-20,54-59) and formats under culture ?? CultureInfo.CurrentCulture (:54, :90), inside the v1.244.0 pin (Directory.Packages.props:19); dates and ratings also format under CurrentCulture. The Identity error-code translations are pinned by IdentityErrorResourcesTests (a known code resolves to Spanish under es, an unknown code falls back to the caller's English message, Tests/Modules/Identity/MMCA.Store.Identity.API.Tests/Localization/IdentityErrorResourcesTests.cs:15). Maturity 4, doubly gated: both arch gates run un-skipped with live thresholds (TranslationCompletenessTests.cs:17 MinimumBaseResources=25, LocalizedTextConventionTests.cs:18 MinimumScannedFiles=40) in the required build-and-test check via MMCA.Store.CI.slnf:53 + deploy.yml:255, and the pseudo-loc E2E suite rides the deploy-gating e2e-gate (job deploy.yml:752, chromium-only :765, in deploy needs at :1084; e2e.yml:394 runs the project unfiltered): PseudoLocalizationTests activates qps-Ploc via the production /culture/set cookie mechanism and asserts the [!! sentinel, the no-horizontal-overflow expression, and a per-page en-US leak probe over /, /catalog, and /login (Tests/E2E/MMCA.Store.E2E.Tests/Workflows/PseudoLocalizationTests.cs:41). Held at 8, not 9: pluralization is only partially mechanism-driven. Common ships StringLocalizerPluralExtensions.cs:46 (.One/.Other suffix resolution) and the cart strings have plural keys, but ProductReviewsPanel.razor:10 renders "(1 reviews)" from a single-form key (ProductReviewsPanel.resx:9), CatalogBrowse.resx:31 is a second unpluralized count key (consumed at CatalogBrowse.CardFormatting.razor.cs:45), the "(s)" keys survive (CartDrawer.resx:22 "{0} item(s)", ShoppingCartList.es.resx:11 "{0} articulo(s)"), and the Spanish plural values drop the accent (the .One and .Other values at ShoppingCartList.es.resx:12-13 and CartDrawer.es.resx:23-24 against the accented base keys at ShoppingCartList.es.resx:11 and CartDrawer.es.resx:22), a user-visible defect recorded as a TD under #27. Layout tolerance is proven on three public pages (PseudoLocalizationTests.cs:41), with no authenticated surface and no RTL locale. Evidence: Tests/Architecture/MMCA.Store.Architecture.Tests/Ui/{TranslationCompletenessTests.cs,LocalizedTextConventionTests.cs}; Sales.UI/.../CartDrawer.razor.cs + OrderDetail.razor.cs (whole-sentence Snackbar.* keys); CatalogUIModule.cs (TitleResource + resx pair); CatalogBrowse.CardFormatting.razor.cs:54-55,84-85 (price display)
28 Front-End Testing & Quality 3 4 8 12/24 bUnit .UI.Tests gate the deploy (CI.slnf), broad state-coverage component tests (loading/empty/error/edge), page-level render/parameter/event tests, shared Common.Testing.E2E, and the Playwright suite gates the deploy (chromium e2e-gate; firefox/webkit stay advisory on the nightly matrix). bUnit breadth is 364 facts across 47 files. Held at 8: no markup or visual-regression tier exists. Evidence: Tests/Modules/{Catalog,Sales,Identity}/MMCA.Store.*.UI.Tests; deploy.yml (e2e-gate in deploy needs); e2e.yml (workflow_call + browsers input)
29 Resilience & Business Continuity 3 4 9 12/27 RTO/RPO per scenario, PITR(7d geo-redundant)+LTR, a drilled restore (recorded PASS 28.9 min), SLO alerts, post-deploy smoke then rollback, single-region risk explicitly accepted. Maturity 4: a dr-freshness job is in deploy.needs and fails the deploy when the last successful dr-drill is stale, backed by a weekly dr-drill.yml cron; the CI-gated GracefulShutdownTests in MMCA.Store.Gateway.Tests covers graceful shutdown. Held at 9, not 10: the DISASTER-RECOVERY.md drill-history table (:207-212, last row dated 2026-07-28 at :212) lags the weekly dr-drill.yml runs that satisfy dr-freshness, a documentation-currency gap. Evidence: .github/workflows/deploy.yml:789 (the dr-freshness job), :1084 (dr-freshness in deploy.needs), :1114 (its success condition); .github/workflows/dr-drill.yml:33 (weekly cron), :56 (60-minute job timeout); Tests/Hosts/MMCA.Store.Gateway.Tests/GracefulShutdownTests.cs; infra/DISASTER-RECOVERY.md:207-212 (drill-history table, the 28.9-min PASS row at :209)
30 Compliance, Privacy & Governance 2 4 8 8/16 Real erasure path (Delete+Anonymize in one UoW, ADR-005), data export, residency + PII enforced by fitness functions, auto-stamped audit fields. The personal-data inventory is executable: PRIVACY.md section 12 lists every [Pii] member with its purpose, lawful basis and retention (PRIVACY.md:121-125, aligned with the policy text), and PiiInventoryTests (sealed subclass of the shared base, Tests/Architecture/MMCA.Store.Architecture.Tests/Privacy/PiiInventoryTests.cs:13) checks it in the merge-gating architecture tier. Held at 8: no consent capture exists in Source, and the retention purge for business data is only partly evidenced; the data-subject-rights surface has a UI, the Profile "Your data" card with export and self-service delete (MMCA.Store/Source/Modules/Identity/MMCA.Store.Identity.UI/Pages/Users/Profile/YourDataCard.razor, mounted at Profile.razor:111). Evidence: DeleteUserHandler.cs:41-67 (OnAfterSoftDeleteAsync); Customer.cs:28 (IAnonymizable), User.cs:30 (IErasableUser); Governance/DataResidencyTests.cs; PRIVACY.md:58-90
31 Cost Efficiency / FinOps 2 4 8 8/16 Right-sized Basic per-service DBs, automated surge-drift guard, IaC budget + alerts, telemetry span filtering, resource tags. Maturity 4: cost-guard is a workflow_call job in deploy.needs (deploy.yml:730, :1084) that must be exactly success (:1113), plus the Monday cron (cost-guard.yml:21). The SQL half of the guard reads both the tier and the service objective and accepts only Basic or Standard S0 (cost-guard.yml:62-68, applied at :102), so a Standard database scaled up to S1 or above (an S3 surge, say) reads as drift; the Basic-or-Standard window itself is the one ADR-081 sanctions for Store (Store commit 398e70fc). The IaC cost posture: all five container apps at 0.25 vCPU / 0.5 Gi (infra/main.bicep:1496,1680,1816,1960,2076), 25% head-based trace sampling (:210), metric export at 300s (:277), the Gateway's Yarp log category held at Warning so the two per-request forwarder lines stay out of ContainerAppConsoleLogs (:232-235, applied in the Gateway env at :1967), the legacy MMCAStore database archived to a bacpac and absent from IaC (:954-966), Log Analytics at 30-day retention (foundation.bicep:36) with a 3 GB/day ingestion cap (:56-58, rationale :37-55), and a daily ACR purge task with a buildcache-only second step (:103, :106). Spend is attributable per service: resources carry the uniform commonTags set (costCenter: 'store', main.bicep:135-141), and each container app and service database adds a per-service service tag (:982 databases, :1458 identity, :1640 catalog, :1764 sales, :1916 gateway, :2045 ui). Held at 8, not 9: the surge guard is read-only (cost-guard.yml:7 "never mutates anything"; :109 prints a manual reset), so the rubric's automated-or-scheduled revert for reversible scale events is unmet in its exact terms (the identical basis holding ADC's §31 at I8); the read-only guard is the recorded posture, because a scheduled revert would act on production unwatched (Deliberate and accepted in the backlog); and the budget is whole-RG with no tag filter (main.bicep:793-821). Evidence: cost-guard.yml:7,21,62-68,102,109; deploy.yml:730,1084,1113; infra/main.bicep:95 (alert email param), :113 (monthlyBudgetAmount), :135-141 (tags), :793-821 (budget, 80% actual / 100% forecast), :1496; infra/foundation.bicep:36,56-58,103,106
32 Dependency & Supply-Chain 2 4 9 8/18 Vulnerability auditing is CI-gated twice: NuGetAudit raises advisory warnings at restore and TreatWarningsAsErrors promotes them to errors (failing build-and-test and every Docker image build), and the supply-chain job's suppress-aware audit gates (exit 1 on any non-suppressed advisory); no advisory is accepted today, and an accepted one is a dated NuGetAuditSuppress item (Directory.Build.props:36-44). Strong provenance: 55 committed packages.lock.json + CPM + lockstep Common + transitive MassTransit-v8 pin + auditSources config, and all three CI restores run --locked-mode (deploy.yml:283,545,655) so committed-lock drift fails the build. The SBOM is a hard gate (43d733f); the license report is report-only by design. The MAUI graph is covered by maui-audit.yml: a weekly (Sundays 06:00 UTC, :35) suppress-aware dotnet list package --vulnerable over MMCA.Store.UI on net10.0-android that exits 1 on any non-suppressed advisory (maui-audit.yml:111-163), the one graph the gating supply-chain job cannot see because the MAUI head is excluded from MMCA.Store.CI.slnf; it is a scheduled audit, not a deploy gate, the Apple TFMs are uncovered on a Linux runner (:19-25), and it mirrors ADC's maui-audit.yml (TD-18). No §32 mechanism behind ADC's I9 is missing here (identical gating audit/SBOM, the same NuGetAudit / NuGetAuditMode pair and lock config); held below 10 by the same transitive-only MassTransit pin caveat as ADC. Evidence: deploy.yml:283,545,655 (locked-mode restores), :527 (supply-chain); Directory.Build.props:35,45-46; nuget.config
33 Developer Experience & Inner Loop 2 4 8 8/16 Aspire one-command local stack (SQL/Redis/RabbitMQ/MailDev/Stripe), cross-repo local.props, fast CI slnf, editorconfig parity, local→Azure parity. Held at 8: the local broker defaults to RabbitMQ with the Service Bus emulator opt-in (STORE_BROKER=servicebus, AGENTS.md:42), the integration tiers need an external SQL Server, and the Service Bus emulator tier gates through recency rather than per PR. Evidence: AGENTS.md:9-78 (imported by CLAUDE.md); local.props.template; MMCA.Store.CI.slnf
34 Architecture Governance & Docs 2 4 9 8/18 Exemplary automated conformance (62 arch fitness-test classes over shared bases + Store-local guards; against ADC's architecture project, Store adds Api/TokenPermissionGrantsTests.cs:40, whose ADC equivalent lives outside the architecture tier in MMCA.ADC.Services.Tests, and carries ADC's CostTagConventionTests, InlineStyleTests and SqlAuditConventionTests too (Governance/CostTagConventionTests.cs, Ui/InlineStyleTests.cs, Governance/SqlAuditConventionTests.cs); both repos run MessageBusBackpressureTests) + a thorough current CLAUDE.md + ADRs canonical in ../adr/, count/range owned by ../adr/README.md (MMCA.Common/FACTS.md:46-48 disclaims owning the ADR range; the framework-wide fitness-function figures are at MMCA.Common/FACTS.md:51), completed by the two-axis scorecard + RemediationBacklog.md (the single per-repo ledger), both committed and kept current per release. Evidence: Tests/Architecture/MMCA.Store.Architecture.Tests/ (64 tracked .cs files: 62 test classes + StoreArchitectureMap.cs + GlobalUsings.cs); CLAUDE.md; committed ArchitectureScorecard.md + RemediationBacklog.md; ADRs in ../adr/ (indexed by ../adr/README.md)

Weighted = Maturity·weight / Implementation·weight.

Scoring notes

  • Two axes per category: Maturity (0-4, process/governance) and Implementation (0-10, substance/execution). The indices are computed deterministically from the table: every non-N/A category is in both denominators.
  • N/A (excluded from denominators): §16 AI-Native Application Architecture (rubric v2, ADR-110: no product feature calls a model); its weight 2 leaves both denominators, Σweight 79.
  • §10 is Messaging & Integration Architecture under rubric v2, at the v2 default weight 3.
  • §27 is scored: ADR-027 (en-US + es with a CI-gated translation-completeness fitness function) supersedes ADR-011's single-locale exclusion.
  • §32 weight = 2 (default; 3 only for the published framework MMCA.Common).
  • The implementation index reads directly against 100%: a 10 is awardable for an almost perfect implementation. The denominators stay ×4 and ×10 so the trend line stays comparable across re-scores.
  • Axis calibration: a new CI gate is an enforcement gain and scores on the maturity axis, not the implementation axis; a CI-cadence change is not a substance regression; documentation drift scores on the implementation axis.
  • Axis-gap reading: §10, §12 and §21 are M3/I8, strong substance with enforcement short of automatic, which is the healthy direction of the gap. The maturity-over-implementation gap sits in the 18 categories at M4/I8, where enforcement is automatic but substance is short of 9.

Cross-repo comparison

How Store relates to MMCA.Common (the framework) and MMCA.ADC (the sibling consumer) is maintained once, for all three repos, in the workspace-internal Docs/Architecture/CrossRepoComparison.md (not published).