Architecture governance
MMCA.Store: Architecture Remediation Backlog
Derived from ArchitectureScorecard.md (two-axis: Maturity 97.8% (309/316) / Implementation 83.9% (663/790) after the rubric v2 rebase of 2026-09-04 (§10 re-weighted to 3 at carried I8, so its implPriority rises 2→3; §16 N/A; Σweight 80→79; no score moved; ADR-110), full re-score 2026-07-28, re-confirmed with no score moves on the 2026-08-14, 2026-08-23, 2026-09-01 and 2026-09-04 full re-scores (the last being the first scored against rubric v2), framework pin v1.185.0, HEAD 04afa39d).
Items are ranked on both scorecard axes, one band per axis (two-axis policy adopted 2026-07-28,
replacing the previous "or a notable implementation gap" wording, which had no number behind it and
so never scheduled anything):
- Maturity band: categories scoring maturity < 4, ranked by priority = (4 − maturity) × weight.
- Implementation band: categories scoring implementation <= 8, ranked by implPriority = max(0, 9 − implementation) × weight. The scheduling target is 9, not 10 (updated 2026-08-14 for the 2026-08-01 recalibration: a 10 is now awardable for an almost perfect implementation, but ranking against 10 would put nearly every strong category in the band and drown the real gaps, so the 9 → 10 rung is recognition earned at re-score time, never scheduled work; 9 mirrors maturity's target of 4).
A category leaves each band independently and reaches the protect list only at maturity 4 AND
implementation >= 9. The indices keep their × 4 and × 10 denominators, so the trend line stays
comparable; the 9-target governs scheduling only. This is Store's first
in-repo remediation ledger; the scorecard is the source of truth for scores, ../MMCA.Common/FACTS.md
for framework-wide facts, and the workspace-internal Docs/Architecture/ArchitectureRemediation.md (not published)
for the cross-repo [C→A] roll-up.
Status legend: ☐ open · ◐ in progress · ☑ done
🔴 Priority: a11y / E2E merge gate (#21, #28, #22)
The former single biggest maturity lever: #28 cleared 2026-07-03; #22 cleared on the 2026-07-17 re-score (the gate flip verified live) and reopened on the 2026-07-28 re-score when the 2026-07-18 chromium-only cut was scored rather than frozen. #21 remains the highest open lever, pending a recorded screen-reader pass.
Maturity band (maturity < 4, ranked by priority)
Computed from the 2026-07-28 re-score, re-confirmed unchanged on the 2026-08-14 re-score (pin v1.152.0), again on the 2026-08-23 re-score (pin v1.160.0, HEAD 063c90dd) again on the 2026-09-01 re-score (pin v1.179.0, HEAD 44d9304c) and again on the 2026-09-04 re-score (pin v1.185.0, all 17 packages lockstep at Directory.Packages.props:8-105, HEAD 04afa39d): 3 categories, 7 gap points.
| priority | # | Category | w | Mat | Recorded lever |
|---|---|---|---|---|---|
| 3 | #21 | Accessibility (a11y) | 3 | 3 | OPEN, recorded below: a dated manual NVDA/VoiceOver pass in store-ACCESSIBILITY-SCREENREADER-PASS.md (needs a human; the results log still holds only the placeholder row, re-verified 2026-08-14 at :68, header :66-67) |
| 2 | #12 | Performance & Scalability | 2 | 3 | OPEN, recorded below: add a latency-regression check to the merge path, or record the monthly k6 cadence as the accepted posture (ADC's stance). Unchanged on the 2026-08-14, 2026-08-23, 2026-09-01 and 2026-09-04 re-scores; the load-freshness break-glass skip re-anchored 2026-09-04 to deploy.yml:729-745 (skip branch at :732, job at :714), and deploy.needs at :999 still contains no perf job |
| 2 | #22 | Responsive & Cross-Browser | 2 | 3 | OPEN, reopened 2026-07-28, posture WIDENED since: add a cross-browser-freshness job to deploy.needs on the dr-freshness pattern, or promote firefox/webkit back into the gate. Since 2026-07-29 the nightly matrix runs ONE alternating engine per week (Mon firefox, Thu webkit; re-anchored 2026-09-01: crons e2e.yml:46-47, browser matrix expression :133-136), so the per-engine blind window is now 7 days. The chromium-only trade-off itself is recorded under Deliberate / accepted |
Ties break by priority desc, then weight desc, then category asc.
- [~] #21 Accessibility, maturity 4 corrected back to 3 (2026-07-11, drift plan D6); impl 7 → 8 DONE. The axe + Playwright suite gates the deploy (
e2e-gate, chromium,workflow_callintoe2e.yml, indeploy.yml'sneeds; qualifier 2026-08-23: the gate is UI-scoped and skippable, see the TD below), and the axe scans broadened 10 → 22 pages (public, shopper, and Catalog/Sales/Identity admin surfaces), lifting impl 7 → 8. The prior maturity-4 was an over-claim: the rubric pairs axe-in-CI with a recorded manual screen-reader pass, so honest maturity is 3, matching ADC on the same rubric. A newACCESSIBILITY-SCREENREADER-PASS.mdrunbook shipped (centralized asstore-ACCESSIBILITY-SCREENREADER-PASS.mdin Websitedocs-src/guides/since 2026-07-20), but its results log is still empty (re-verified 2026-08-14: placeholder row only; the axe suite meanwhile grew 22 → 23 scans with a dark-palette home scan,AccessibilityTests.cs:327-340). Maturity 3 → 4 lever: record a dated manual SR pass in the new runbook (needs a human + NVDA/VoiceOver against the running Aspire app; cannot be done headless). - TD · The deploy-gating a11y/E2E run is UI-scoped, so a backend-only merge deploys with no axe or Playwright run (found 2026-08-23; affects #21, #22, #28). The
e2e-gatejob runs only whenneeds.changes.outputs.ui == 'true'(re-anchored 2026-09-03:deploy.yml:645, job at:634, rationale comment:636-638, chromium-only input at:648, the 2026-07-29 Actions-minute saving), and thedeployjob deliberately tolerates a SKIPPEDe2e-gate: it is the one gate allowed to besuccessORskippedwhile every other gate must besuccess(deploy.yml:1038, inside the gate block:1026-1039, withe2e-gatein the deployneedslist at:999). On a backend-only merge, therefore, the 23 WCAG 2.1 AA axe scans (Tests/E2E/MMCA.Store.E2E.Tests/Workflows/AccessibilityTests.cs, dark-palette scan at:327-340) and the Playwright workflow suite do not run at all, and the chromium-only blind window priced under Deliberate / accepted becomes 100% for all three engines. This ledger previously asserted the axe suite gates the deploy unconditionally; that claim is now qualified where it appears. Backend half CLOSED 2026-09-03 (Store PR #123, drift D1). The part of this finding that mattered most, a backend-only merge deploying with NO test execution at all, is gone:backend-test-gate(deploy.yml:347) carries the exact complement ofe2e-gate's condition (:349against:645) and runs theMMCA.Store.CI.slnfunit + architecture + bUnit tier, so exactly one of the two runs on every code deploy; both sit indeploy.needs(:999) and both are permitted to beskipped(:1038-1039). Still open (browser half): a backend-only merge still runs no axe scan and no Playwright suite, so the UI-scoping itself remains neither made unconditional nor recorded as a deliberate accepted trade-off alongside the chromium-only entry. That is the remaining lever. - #28 Front-End Testing, maturity 3 → 4 DONE (2026-07-03) / impl 6 → 8 DONE (2026-07-11, drift plan D7). The E2E + axe deploy gate shipped with #21; bUnit breadth grown to 214
[Fact]/[Theory]across 40 files (Catalog 63 / Sales 126 / Identity 25) with loading/empty/error/edge state coverage, the full CI gate green at 1393/1393. - [~] #22 Responsive & Cross-Browser, maturity 3 → 4 GRANTED on the 2026-07-17 re-score; basis went STALE the next day (drift recorded on the 2026-07-23 verification pass); maturity REOPENED 4 → 3 on the 2026-07-28 re-score. The 2026-07-18 Actions-minute reduction (commit
777348ec, mirroring ADC's) cut the deploye2e-gateto chromium only (deploy.yml:494,browsers: '["chromium"]', rationale comment:483-488), so firefox/webkit now run only on the scheduled matrix (re-anchored 2026-08-14:e2e.yml:143keeps themcontinue-on-error,:133-135selects the engine; since 2026-07-29 the schedule runs ONE alternating engine per week, Mon firefox / Thu webkit per the crons at:37-47, so each engine is blind for 7 days), and the granted basis, "all three engines the gate invokes CAN fail a deploy", no longer holds. There is no cross-browser freshness job indeploy.needsto bound the blind window either, so cross-engine verification is convention-enforced (Consistent=3), not automatic. Scorecard §22 is now M3/I8, matching ADC's twenty-second cycle on identical evidence; the proposed Implementation 8→7 was adversarially REJECTED (a CI-cadence change is not a substance regression). Maturity 3 → 4 lever: add across-browser-freshnessjob todeploy.needson thedr-freshness/load-freshnesspattern, which bounds staleness without paying for three engines per deploy, or promote firefox/webkit back into the gate. The chromium-only cost trade-off is recorded under Deliberate / accepted so the choice stays conscious rather than silently low. Grant provenance with anchors as of 2026-07-17:e2e.yml:76scopedcontinue-on-errorto scheduled non-chromium runs (now:117),deploy.yml:315invoked all three engines (gate now:417-423),deploy.yml:429pute2e-gatein deployneeds(now:634). History of the reopen-and-fix below. The wave-5 change passedbrowsers: ["chromium", "firefox", "webkit"]into thee2e-gatecall, so all three engines RUN in the gate, but the non-chromium legs cannot FAIL it:e2e.yml:71still setscontinue-on-error: ${{ matrix.browser != 'chromium' }}, anddeploy.yml:433's own inline comment describes e2e-gate as chromium-only. The 2026-07-16 re-score held maturity 3 on exactly this evidence and the candidacy was declined. The green-soak history (2026-07-09 through 2026-07-11, plus thed057afcthree-engine catch) still stands as soak evidence. Maturity 3 → 4 lever: remove thecontinue-on-errorconditional for the gate-invoked firefox/webkit legs (or gate them behind their own required jobs) once the soak is judged sufficient; nightly-matrix legs may stay advisory. Lever SHIPPED same day (2026-07-16):continue-on-erroris nowgithub.event_name == 'schedule' && matrix.browser != 'chromium', so all three engines the gate invokes (deploy.yml:315) CAN fail a deploy while nightly non-chromium legs stay advisory flake alarms; the stale chromium-only comments ine2e.yml/deploy.ymlcorrected. Soak judged sufficient: job-level green nightly matrices 2026-07-09 through 2026-07-16, with the sole 2026-07-12 red being the all-three-engines product defect fixed ind057afc(a true positive, not flake). Maturity 3 → 4 candidacy recorded for the next re-score.
🟠 Priority: execution-quality gaps (impl, not maturity)
Implementation band (implementation <= 8, ranked by implPriority)
Ranked 2026-07-28 when the ledger gained its second ranked axis. Until then the items in this
section were closed history plus two open levers, with no ranking and no inclusion rule, which is
why maturity reached 98.4% while implementation sat at 83.6%. Computed from the current scorecard
(2026-07-28 full re-score, re-confirmed unchanged on the 2026-08-14 re-score, again on the
2026-08-23 re-score, again on the 2026-09-01 re-score, and again on the 2026-09-04 re-score at pin v1.185.0 / HEAD 04afa39d):
20 categories, 48 gap points (2026-09-04 rubric v2 rebase: the #16 row retired (−2) and #10 re-weighted to 3 (+1)), the largest of the three repos. (The former "attainable ceiling"
comparison line is retired per the 2026-08-01 recalibration; the index reads against 100%.) Levers
are cited only where this ledger or the scorecard already records
one; an unnamed lever is named at the next re-score, never invented here.
| implPriority | # | Category | w | Impl | Recorded lever |
|---|---|---|---|---|---|
| 4 | #20 | Design System & UI Consistency | 2 | 7 | OPEN, recorded below, re-verified 2026-08-23: ProductList's three cited attributes are converted (commit a1de5a89, semantic classes in store.css:28-40), but 30 Style=/CellStyle= occurrences remain across 14 razor files (count corrected from 31 on 2026-08-23, a counting fix: zero razor files changed), five byte-identical to the new classes (CategoryList.razor:23,79,88; OrderList.razor:22; CustomerList.razor:81); the sweep onto the now-existing classes is the lever, and the classes are mirrored in the MAUI head's app.css:38,42,47, a second sweep target |
| 3 | #7 | Microservices Readiness | 3 | 8 | not yet identified (all per-service DBs on one physical server is a recorded accepted cap) |
| 3 | #10 | Messaging & Integration Architecture | 3 | 8 | **Rubric v2 (2026-09-04, ADR-110): category replaced in place, carried at the prior I8 and re-weighted to the v2 default 3 (implPriority 2→3), re-scored against the new criteria on 2026-09-04 and CONFIRMED M4/I8 (broker topology per ADR-066, poison handling per ADR-087, saga compensation, the frozen event contract, the both-tier broker freshness gate at deploy.yml:770/:999); the lever for 9 is not yet identified, name it at the next re-score |
| 3 | #11 | Security | 3 | 8 | OPEN, recorded below (lever named 2026-08-18 by the ADR audit): Store's identity seeder has no environment gate, so weak plaintext seed accounts are created in every environment, production included |
| 3 | #18 | UI Architecture & Components | 3 | 8 | residual inline-style logic (the basis on which the 2026-07-16 impl bump to 9 was rejected) |
| 3 | #19 | State Management & Data Flow | 3 | 8 | lever candidate named 2026-08-14: route the drawer open/close through the service notify path. IsDrawerOpen is publicly settable on the contract (re-anchored 2026-09-01: ICartStateService.cs:41) and mutated outside NotifyStateChanged by the component (CartDrawer.razor:3 two-way bind, CartDrawer.razor.cs:153 direct set; re-anchored 2026-09-04 after the ShoppingCarts/ folder move), the red flag on which the 8→9 bump has now been rejected three times (2026-07-28, 2026-08-14 and 2026-09-01; the 2026-09-01 rejection also named the partially applied M88 superseded-load guard and the withdrawn IUiReadCache client cache) |
| 3 | #21 | Accessibility (a11y) | 3 | 8 | not yet identified (the dated SR pass is the maturity half; #21 is also in the maturity band) |
| 3 | #28 | Front-End Testing & Quality | 3 | 8 | not yet identified |
| 2 | #5 | Vertical Slice Architecture | 2 | 8 | the layered-by-project hybrid, recorded as a deliberate design choice capping impl at 8 |
| 2 | #6 | CQRS & Event-Driven | 2 | 8 | not yet identified |
| 2 | #12 | Performance & Scalability | 2 | 8 | not yet identified |
| 2 | #15 | Best Practices & Code Quality | 2 | 8 | lever named 2026-08-14 (the verify pass proposed I7 on these; user-adjudicated hold at 8), narrowed 2026-09-01: (1) CLOSED: the expired GHSA-2m69-gcr7-jv3q audit suppression is gone from Directory.Build.props, whose NuGetAuditSuppress block is now three System.Private.Uri advisories at :8-12 with its rationale at :7; (2) still open, re-scoped: the three undocumented global NoWarn codes CS1591/RMG020/EXTEXP0001 remain at :26 (the fourth code S8970 IS documented at :22-25), but the "uncommented duplicates" half is now half true, since :35-43 carries a rationale for the test-wide suppressions while five test csprojs still declare their own NoWarn; (3) half CLOSED 2026-09-03 (Store PR #123, drift D2): the MAUI head is still in neither .slnf (re-verified against MMCA.Store.CI.slnf, which lists only UI.Web:24 and UI.Web.Client:25), so the analyzers/TWAE half stays open, its build enforcement still happening only in a full local solution build. The vulnerability-audit half is closed by maui-audit.yml: a weekly (Sundays 06:00 UTC, :35) suppress-aware dotnet list package --vulnerable --include-transitive over Source/Hosts/UI/MMCA.Store.UI/MMCA.Store.UI.csproj on net10.0-android, exiting 1 on any advisory not in the NuGetAuditSuppress set (:103-151). Advisory, not a deploy gate, and the Apple TFMs stay uncovered because a Linux runner cannot restore them (:19-25) |
| 2 | #16 | 2 | 8 | Retired 2026-09-04 (rubric v2, ADR-110): §16 is now AI-Native Application Architecture and N/A for this repo. The former category's coupling and tech-debt criteria score under #34, lockstep upgrades under #32, onboarding under #33. Struck through for the record; not counted in the band total. | |
| 2 | #22 | Responsive & Cross-Browser | 2 | 8 | not yet identified (the chromium-only gate is the maturity question, tracked above; the 2026-07-28 proposal to drop impl to 7 was adversarially rejected) |
| 2 | #23 | Front-End Performance | 2 | 8 | not yet identified |
| 2 | #24 | Forms, Validation & UX Safety | 2 | 8 | OPEN, narrowed 2026-09-01 (drift plan D4), recorded below: the authoring-mechanism half is CLOSED (all 4 create forms now source field rules from a DataAnnotations form model through Common's ModelValidation bridge, and FormsConventionTests was tightened to ADC's marker shape), so the remaining lever is only the rule-engine half: client validation is DataAnnotations-level, not full FluentValidation parity with the server rules. Score unchanged pending the next re-score |
| 2 | #25 | Navigation & Information Arch | 2 | 8 | lever named 2026-09-04 (the verify pass rejected a proposed M4→3 and confirmed I8): store-NavigationFlow.md:166 claims each actor's menu shows only the routes in its diagram, but the framework shell renders /profile/sessions for every signed-in user (MMCA.Common.UI/Layout/NavMenu.razor:142) and /notifications/send is routable behind a bare [Authorize] (NotificationSend.razor:6) with no notification services wired in the Store UI host; reconcile the doc and the shell (hide or document both routes). Also re-anchored: the three *.UI.Tests projects sit at MMCA.Store.CI.slnf:40,46,52, not :39,45,51 |
| 2 | #30 | Compliance, Privacy & Governance | 2 | 8 | not yet identified |
| 2 | #31 | Cost Efficiency / FinOps | 2 | 8 | not yet identified |
| 2 | #33 | Developer Experience & Inner Loop | 2 | 8 | the §33 I8→9 candidacy recorded 2026-07-16 on the Service Bus emulator tier was NOT granted on 2026-07-28 on the grounds that the tier was nightly and non-gating; that grounds is retired 2026-09-01 (drift plan D1): the servicebus-emulator-smoke job is authoritative (no continue-on-error) and the cross-service-freshness deploy gate now requires BOTH it and cross-service to have concluded success in the same nightly run, mirroring ADC's 2026-08-31 TD-17 promotion. The residuals are that the tier still needs Docker (so it stays out of both .slnf filters and reaches the deploy chain through the recency check rather than per PR). Candidacy re-recorded; score unchanged pending the next re-score. See Deliberate / accepted |
| 1 | #27 | Internationalization (i18n) | 1 | 8 | OPEN, half-closed 2026-08-14: the $-glyph half is FIXED in Common (per-currency symbol resolution, MoneyExtensions.cs:18-20,54-59, re-anchored 2026-08-23; Common change 2026-08-05, inside the v1.160.0 pin). Remaining: amounts still format with CultureInfo.InvariantCulture (:69-70) and pluralization stays the "{0} item(s)" / "{0} articulo(s)" workaround (CartDrawer.resx:20, ShoppingCartList.es.resx:11). The fix lands in MMCA.Common, so it is [C→A], not Store-local (2026-08-23 caveat: re-verified against Common source at HEAD, not the published v1.160.0 package body) |
- #11 Security, OPEN (found 2026-08-18 by the ADR audit): the identity seed accounts have no environment gate. The framework leaves the choice to the app:
IdentityModuleDbSeederBase.ShouldSeeddefaults totrue(IdentityModuleDbSeederBase.cs:57), directly under a security notice saying the seed credentials are deliberately weak plaintext values and that deployed environments must disable seeding (:31-35). ADC overrides it behindSeeding:IncludeSampleUsers, default false, so a production host that sets nothing seeds no accounts (MMCA.ADC.Identity.API/IdentityModuleSeeder.cs:22-30). Store does not override it: itsSeedAsyncconstructs and runs the DB seeder unconditionally (MMCA.Store.Identity.API/IdentityModuleSeeder.cs:19-24, re-anchored 2026-09-04), and ADR-030 records that the startup owner runs every module seeder in all environments. The seeded set is an admin account plus a customer account with weak plaintext passwords, one of them a real personal address (MMCA.Store.Identity.Infrastructure/.../IdentityModuleDbSeeder.cs:17,30-32); the only thing between them and production is the per-accountEmailExistsAsyncprobe (:38-43), which skips an account that is already there but does nothing to stop the first seed. Lever: mirror ADC (overrideShouldSeedon aSeeding:IncludeSampleUsersflag defaulting to false), then check the deployed Store Identity database for already-seeded accounts and rotate or remove them. Not a re-score on its own: it is recorded here so the #11 lever stops reading "not yet identified". - #14 Testability, impl 6 → 9. DONE (2026-07-01 wave, re-scored 2026-07-02). The floor step now measures Store's own code (reportgenerator
+MMCA.Store.*;-*.Tests) at floor 42.0 with46% actual (54% measured) after Stripe money-path, Sales UI, and GDPR-handler unit tests. Integration-coverage expansion (2026-07-06): ~100 new integration tests over real SQL closed the money-path gaps (Stripe webhook signature contract, order state machine, deliberate 404-not-403 order ownership), GDPR erasure/export end-state, refresh-token rotation, preferences, cross-servicedeploy.yml:82-86), the ADC-parity self-filtered gate; bUnit page-level breadth reached ADC parity (8 files). Unit-coverage program (2026-07-05): floor ratcheted 42.0 to 51.6 (ProductVariantChangedconsistency, checkout concurrency, contract guards (OpenAPI + RFC 9457) for Sales/Identity, and the ProductImages/PUT leftovers;[Idempotent]was wired onto the Sales money POSTs with a replay contract test. The Catalog+Sales fixtures were consolidated ontoSqlServerIntegrationTestFixtureBase. Cross-service broker tier (2026-07-11, drift plan D5): a non-gating nightlyMMCA.Store.CrossService.IntegrationTests(Testcontainers RabbitMQ + SQL,cross-service-tests.yml) now exercises the genuine outbox-to-broker-to-consumer round-trip (CatalogProductVariantChangedto Sales' zero-stockInventoryItem); the first run needs a manualworkflow_dispatch(done 2026-07-11, green). Gated by recency same day (remediation wave 6): across-service-freshnessjob indeploy.needsfails a deploy when the latest successful nightly is older than 3 days (mirrors ADC TD-02; the Testcontainers workflow itself stays out of the deploy chain). Deliberately skipped: dedicated rate-limit fixtures (the WAFs neutralize the limiter; a tight-limit variant is low value for the volume, revisit only if abuse is observed). - #23 Front-End Performance, impl 6 → 8. DONE (2026-07-03, drift plan D4 + D10). Public
CatalogBrowsemoved to server-side paging (GetPagedAsync+ boundedMobileInfiniteScrollList); cart enrichment resolves names via the targetedproducts/variant-lookupbatch endpoint (CartStateService.cs:282-296); Core Web Vitals (LCP/CLS/TTFB/FCP) are measured per E2E run and uploaded as CI artifacts (Workflows/WebVitalsTests.cs). Accepted trade-offs (2026-07-02 approval): price sorts fall back to Newest; search is Name-contains.
🟡 Priority: minor / accept-or-polish
- #32 Dependency & Supply-Chain, impl 7 → 8. DONE (2026-07-03, drift plan D8 + D9). Vulnerability gate is NuGetAudit +
TreatWarningsAsErrorsat restore, which failsbuild-and-test(aneeds:ofdeploy) and every Docker image build; accepted advisories are the documentedNuGetAuditSuppressset (Directory.Build.props:16,8-12,38-40). The SBOM became a hard gate in43d733fwithsupply-chainadded to deploy'sneeds. All three CI restores now run--locked-mode(committed-lock drift fails the build) and the vuln report is promoted to the suppress-aware gating audit mirroring ADC. - [~] #20 Design System, impl 7 → 8 lever OPEN (marker corrected 2026-07-17: the wave-6 done-claim over-reached; the 2026-07-16, 2026-07-17, 2026-07-28, and 2026-08-14 re-scores all hold §20 at M4/I7; anchors refreshed 2026-08-14: the previously cited
ProductList.razor:23,95,104residuals are CONVERTED by commita1de5a89to.list-search-field/.grid-cell-count/.grid-cell-actionsinstore.css:28-40, but 31Style=/CellStyle=occurrences remain across 14 razor files, five byte-identical to those classes:CategoryList.razor:23,79,88,OrderList.razor:22,CustomerList.razor:81, plus CatalogBrowse/CatalogProductDetail/OrderLinesPanel/CustomerDetail; the 2026-08-14 proposed impl 7→8 was adversarially REJECTED on exactly this residual set). Maturity 4 stands (2026-07-01). The brand-color token guardBrandColorTokenTestsshipped in5fbd003(CI-enforced, both UI hosts' home CSS repointed tovar(--mmca-primary)). The remaining impl lever landed: every plain-HTML inlinestyle=in Catalog.UI and Sales.UI moved verbatim into 17 new scoped.razor.cssfiles as semantic classes (cart drawer, product images/variants, catalog cards, order/inventory pages; classes only added, DOM structure unchanged). Recorded residuals: MudBlazorStyle=/CellStyle=parameters on components with no plain-HTML ancestor in their own markup, which Blazor CSS isolation cannot target without a DOM-changing wrapper (list in the wave-6 commit); no hard-coded brand hex anywhere. CI.slnf 1397 green incl. all fitness gates. - #9 API & Contract, impl 8 → 9. DONE (2026-07-03, drift plan D16). The v2 demonstration shipped:
ServiceInfoControllercarries[ApiVersion("1.0", Deprecated = true)]+[ApiVersion("2.0")]mirroring ADC, and the two deploy-gating contract-guard integration tests (Contract/ApiVersioningTests+Contract/OpenApiContractTests, mirroringMMCA.ADC.Conference.IntegrationTests/Contract/) run in theintegration-testsjob. (Scorecard §9 refreshed to M4/I9 on 2026-07-11, drift plan D12.) - [~] #24 Forms, maturity 3 → 4 DONE (2026-07-11, drift plan D11); impl 8 → 9 lever OPEN (marker corrected 2026-07-17: the wave-6 done-claim over-reached; the 2026-07-16 and 2026-07-17 re-scores both hold §24 at M4/I8 because client validation stays MudForm-level, not full FluentValidation parity with the server rules). Maturity 4 is earned by the CI-gated
FormsConventionTests(MinimumCreateForms=4) machine-enforcing the four create forms' guard/dirty/validated-MudForm/Requiredmarkers, matching ADC. The impl lever landed: client-side parity added where server rules were genuinely unmirrored: email-format validation (mirroringCustomerEmailRules' exact algorithm) on CustomerCreate + CustomerDetail, and per-field NewPassword min-length/128-cap plus confirm-match on Profile change-password (localized en+es keys). Audited-and-already-covered rules (Product/Category/Inventory lengths, ranges, SKU cap) and deliberately server-only rules (SKU uniqueness needs the DB) recorded in the wave-6 commit. No FormsConventionTests markers weakened. Update 2026-09-01 (drift plan D4): the authoring-mechanism half of the lever is CLOSED. All four create forms (CategoryCreate,ProductCreate,CustomerCreate,InventoryItemCreate) now source their field rules from a DataAnnotations form model through Common'sModelValidationbridge (Model="_model"plusValidation="@_validate"plusModelValidation.IsRequired) instead of spelling requiredness per field in markup, which is the shape ADC's 8 Conference create forms already used;FormsConventionTestswas tightened to ADC's marker set and gained Store's copy of the requiredness fact that reads off the model, withMinimumCreateFormsstill 4 so the gate stays non-vacuous. The Profile-form password fields stay inline by design on both sides. What remains of the impl lever is the rule-engine half only: the client rules are DataAnnotations, not full FluentValidation parity with the server rule set. Score arithmetic left to the next re-score. #7 Microservices / single shared DBNOT A GAP (corrected): Store runs database-per-service (Store_Catalog/Store_Sales/Store_Identity); the legacyMMCAStoreDB is a retained read-only archive only.
🐞 Defect-fix wave (2026-07-05)
Four reviewed product defects fixed in one wave; every behavior change flipped its pinning test in the same change.
- S-1 Stripe network errors escaped the Result pattern.
StripePaymentServicenow handlesHttpRequestExceptionin both resilience predicates (retry + circuit breaker) and maps it toPayment.Stripe.SessionCreationFailed/Payment.Stripe.SessionRetrievalFailedinstead of letting it propagate past the catch blocks. - S-2 Zero-decimal currency guard. Checkout-session creation validates the currency against a deliberate two-decimal allow-list (USD, EUR; must be extended together with
Currency.All) before the* 100smallest-unit conversion, returning the newPayment.Stripe.UnsupportedCurrencyerror, and roundsAwayFromZeroinstead of truncating fractional cents. - S-3 BuyNow drawer half-failure parity.
CheckoutAndPayAsyncnow returnsCheckoutOutcome(StripeUri, OrderId); on a payment-session failure it also persists the orphan-recovery token (previously only the success path did) andCartDrawernavigates to/orders/{id}with the order-id warning, matching Catalog's BuyNow fallback. - S-4 Case-sensitive Admin check in the GDPR handlers.
DeleteUserHandler/ExportUserDataHandleruse the new case-insensitiveUserRole.IsAdmininstead of!= UserRole.Admin, so a lowercaseadminrole claim authorizes the cross-user path. - Reviewed, not fixed (deliberate):
CartStateService.InitializeAsyncloading the cart without raisingOnChangestays pinned intended behavior (initialization is silent; only user-visible mutations notify).OrderDetail's missing[Authorize]remains a recorded deliberate choice (E2E gap program review), out of scope for this wave.
Deliberate / accepted (record the choice; don't silently leave low)
- ADR-042 device capability abstraction (latent, drift plan D8). The core extension point is converged (Store wires browser + MAUI capabilities via
UseMauiDeviceCapabilities/AddBrowserDeviceCapabilitiesand renders the sharedOfflineBanner), but Store consumes no further capability in its own product UI: noExternalLink, noDeviceUIModule/DeepLinkListener, no app actions. Because no Store product page carries an external anchor today, the WebView dead-end risk is LATENT, not a live defect, so there is nothing to convert now. AdoptExternalLinkon any future Store product page that grows an external anchor, and register a StoreIUIModulewithDeepLinkListener(the way ADC'sDeviceUIModuledoes) if a Store MAUI feature surface is ever wanted. The framework side is complete in Common (18 capability contracts + theMMCA.Common.UI.Mauipackage), so this is consumer-side only, not[C→A]. #5 Vertical Slice (M3)RESOLVED (2026-07-17 re-score): §5 is M4/I8, granted on the CI-gatedSliceCohesionTests(sealed subclass of the shared non-vacuous base, in the deploy-gatingMMCA.Store.CI.slnf:53), the identical gate ADC credits at M4/I8. The layered-by-project hybrid remains a deliberate design choice, now correctly recorded as an implementation-axis cap (holds impl at 8), not a maturity deduction. Moved to the protect list.#27 Internationalization N/ARETIRED (2026-07-02): ADR-027 superseded ADR-011; Store ships full en-US + es localization with the CI-gatedTranslationCompletenessTests. §27 is scored and included in the indices. Updated 2026-07-03 (i18n completion sweep): §27 is M4/I8 with zero residual hard-coded literals (35 snackbars incl. cart/checkout/Stripe, 33 breadcrumb labels, nav items, both StoreHome hosts), a second CI gate (LocalizedTextConventionTests), the completeness floor raised 20→25, and MudBlazor chrome localized via the framework'sResxMudLocalizer. Impl 8→9 lever DONE (2026-07-11, remediation wave 6):Tests/E2E/MMCA.Store.E2E.Tests/Workflows/PseudoLocalizationTests.csextends the pseudo-loc text-expansion evidence to Store's own public pages (/,/catalog,/login): activatesqps-Plocvia the production/culture/setcookie mechanism (the circuit handshake carries cookies, not query strings), asserts the[!!sentinel, Common's exact no-horizontal-overflow expression, and a per-page resx-owned en-US leak probe, plus a default-culture sentinel guard. No host/AppHost change needed; rides the deploy-gating chromium e2e-gate (first genuine run in CI). §27 Implementation 8→9 candidacy recorded for the next re-score. Candidacy GRANTED on the 2026-07-17 re-score (user-adjudicated: the lever's test is real and rides the deploy-gating chromium e2e-gate): §27 was M4/I9. REVERSED on the 2026-07-28 re-score: §27 is M4/I8 and is back in the implementation band. Not a regression, and not a withdrawal of the lever:PseudoLocalizationTests.cs:64,100is intact and un-skipped and both arch gates still run inMMCA.Store.CI.slnf:53. The I9 was an over-grant because it scored the lever rather than the category: two of the rubric's five criteria are unmet in current code, namely culture-aware number formatting (Money.ToDisplayString()hard-codes a$glyph and formats withCultureInfo.InvariantCulture,MMCA.Common .../MoneyExtensions.cs:20,41, an explicit rubric red flag) and mechanism-driven pluralization (the"{0} item(s)"/"{0} articulo(s)"workaround,CartDrawer.resx:20,ShoppingCartList.es.resx:11). The first pass proposed 7; 8 was adjudicated. Both defects live in shared MMCA.Common code, so the fix is[C→A]and the same deduction may apply to Common's and ADC's §27 at their next re-scores. Update 2026-08-14: the$-glyph half is FIXED (Common resolves the symbol from the price's own currency since 2026-08-05,MoneyExtensions.cs:18-20,54-59, re-anchored 2026-08-23, inside the v1.160.0 pin Store consumes); theCultureInfo.InvariantCultureamount formatting (:69-70) and the pluralization workaround remain, so §27 holds I8 and stays in the implementation band (re-verified 2026-08-23:CartDrawer.resx:20,ShoppingCartList.es.resx:11).- Single-region deployment: accepted in
infra/DISASTER-RECOVERY.md(real load doesn't justify multi-region cost). - All per-service DBs on one physical SQL server: logical isolation complete; shared server for cost (minor §7/§8).
- 2026-07-16 re-verification note: #9 (M4/I9) and #32 came back FLAG on the full re-score (first-pass scorers proposed regressions that the adversarial verify pass disproved against committed evidence). #9 stands at M4/I9. #32 was re-adjudicated the same day by the drift-analysis fold: a capability-level ADC comparison (adversarially verified) found no mechanism behind ADC's I9 that Store lacks, so §32 is now M4/I9; the earlier FLAG had reasoned from stale scorecard text (including a stale 49 lock-file count, actual 55).
- ADR-043 adoption (mobile deep links / app association / native OAuth callback): recorded DEFERRED (2026-07-16 drift fold). The drift analysis lands this in Store, but adoption is feature-scale (Store-scheme deep links, iOS/Android manifest entries, associated domains) and rides the same trigger as the recorded ADR-042 latency: adopt when a Store MAUI surface is actively wanted. Not scheduled; revisit with the ADR-042 entry above.
- #33 broker-parity tier SHIPPED (2026-07-16, mirrors ADC):
Tests/Integration/MMCA.Store.ServiceBusEmulator.IntegrationTestsruns MassTransit v8 against the official Service Bus emulator (pinned 2.0.1) with the realProductVariantChangedcontract, proving admin-plane topology creation + the AMQP round-trip nightly incross-service-tests.yml(new job, samecross-service-freshnessdeploy gate). Closes the local-RabbitMQ vs prod-Service-Bus red flag with automation instead of documentation; §33 I8→9 candidacy recorded for the next re-score. Candidacy NOT granted on the 2026-07-28 re-score: the tier still exists but is nightly and non-gating, and its project (Tests/Integration/MMCA.Store.ServiceBusEmulator.IntegrationTests) is in neither solution filter, reaching the deploy chain only through the recency check (re-anchored 2026-09-04: thecross-service-freshnessjob atdeploy.yml:770, in deployneedsat:999). §33 holds M4/I8 and stays in the implementation band. Tier PROMOTED TO GATING 2026-09-01 (drift plan D1), so the "nightly and non-gating" half of that refusal no longer applies:continue-on-erroris off theservicebus-emulator-smokejob incross-service-tests.yml:151, with an explicit do-not-re-add note at:143; its fixture now starts the bus once on the collection fixture with wall-clock-bounded startup phases (the shape ADC uses), andcross-service-freshnessindeploy.ymlrequires BOTHcross-serviceandservicebus-emulator-smoketo have concluded success in the same nightly run (the both-jobsjqfilter atdeploy.yml:824, success message:827, failure message:835; re-anchored 2026-09-04), so a Service-Bus-only regression blocks the next deploy. Mirrors ADC's 2026-08-31 TD-17 promotion, and ADR-066 is refreshed to record the gated end state in both consumers. Honest residual: the tier needs Docker, so it stays out of both solution filters and gates through recency rather than per PR. §33 I8→9 candidacy re-recorded on this basis; this entry records the mechanism, not a score move, which the next/update-scorecardrun owns. - ADR-044 adoption (native push, third notification channel): recorded DEFERRED (2026-07-16 drift fold). Store has no user-notification pipeline at all (no ADR-024 inbox, no SignalR channel), so ADR-044 adoption means adopting the whole notification stack first: a product decision, not remediation. Record here so the gap is conscious; schedule only if Store wants user notifications.
- Chromium-only deploy E2E gate: accepted CI-cost trade-off (recorded 2026-07-28). The 2026-07-18 Actions-minute reduction cut the deploy-gating
e2e-gateto a single engine (re-anchored 2026-09-04: job atdeploy.yml:634, rationale:636-644,browsers: '["chromium"]'at:648) and left firefox/webkit on the Mon/Thu advisory matrix (re-anchored 2026-09-01: thebrowsermatrix expression ate2e.yml:133-136, whose alternating branches are:134firefox and:135webkit, with the schedule-scopedcontinue-on-errorat:143; the entry's former:133-135engine-selection anchor is superseded by that range, not dropped). The saving is real and the decision stands; what was missing was the record, so the ledger predicted a reopen at line 26 and in the protect list without anyone having decided anything. Recording the trade-off does not restore the score: the rubric's maturity 4 is "enforced automatically", and a convention-enforced check is a 3, so §22 is scored M3 and sits in the maturity band with a named lever. The two are complementary: the score reflects what CI enforces, this entry reflects why. Revisit if a webkit-only or firefox-only defect ever reaches production, which is the risk being priced. Cadence update (verified 2026-08-14, re-confirmed 2026-08-23, 2026-09-01 and 2026-09-04): since 2026-07-29 the scheduled matrix runs ONE alternating engine per week (Mon firefox, Thu webkit; cronse2e.yml:46-47, engine selection:134-135), not both engines twice weekly, so the priced blind window per engine is now 7 days, wider than originally recorded. Content gap noted 2026-08-23: this entry prices the ENGINE dimension only. The gate is also UI-SCOPED (re-anchored 2026-09-04:deploy.yml:645) and a skippede2e-gatedoes not block the deploy (deploy.yml:1038, gate block:1026-1039, withbackend-test-gatenow the complement at:1039), so on a backend-only merge the priced blind window is 100% for all three engines; that second, unrecorded hole is tracked as the TD under #21 in the maturity-band section, pending a decision to either enforce or accept it. - 2026-07-28 FLAG carry-forward (#19, #30). Both categories came back FLAG on the full re-score: first-pass proposals (#19 Implementation 8→9; #30 M4/I8→M3/I7) that the adversarial verify pass rejected against evidence re-read at HEAD
8d4af68c. Both hold their prior M4/I8 and their implementation-band rows are unchanged, still with no named lever. #19's rejection was specific: no §19 substance landed since the prior pin (the UI diff is culture-invariant string mechanics, payment-poll cadence tuning, and batched-lookup round-trip cuts, all §12/§23 work), and a minor red flag persists in a publicly settableIsDrawerOpenon the shared scoped state service, mutated directly by the component outside the notify path. - 2026-08-14 FLAG carry-forward (#5, #15, #17, #19, #20, #21). Six categories came back FLAG on the full re-score, every one an adversarial rejection of a proposed first-pass uplift against evidence re-read at HEAD
9571a963, none a found regression: #5 held I8 (horizontal folders inside module Application layers; generic-CRUD slices on shared framework handlers; only three bespoke query types), #17 held I9 (no pre-prod Bicep validation, SQL public network access, prod-only environment), #19 held I8 for the second consecutive cycle (IsDrawerOpen, now a named lever in its band row), #20 held I7 (the ProductList conversion covered ~3 of 34 occurrences), #21 held M3/I8 (placeholder SR log; one added dark-mode scan is a Strong-band increment). #15 is the one adjudicated case: the verify pass proposed a correction to I7 on three suppression-hygiene gaps (expired GHSA-2m69-gcr7-jv3q suppression, undocumentedNoWarncodes, MAUI head outside CI); the user adjudicated a hold at the prior I8, and the three gaps are recorded as #15's named lever in the band table above. - 2026-08-23 FLAG carry-forward (#5, #6, #7, #9, #12, #20, #31). Seven categories returned FLAG on this re-score; none is a found regression and every one keeps its prior score under the merged-prior rule. #9 holds M4/I9 and stays on the protect list (a proposed downgrade was rejected: the contract-guard evidence base grew to seven files plus the frozen gRPC proto contract; only the scorecard row's narration was stale, now corrected). #12 holds M3/I8 and #20 holds M4/I7, both with their band rows and levers unchanged (re-verified this run:
deploy.yml:613/:626-643for #12, the 30 residualStyle=/CellStyle=occurrences for #20). #6, #7, and #31 hold I8 with no lever-bearing evidence surfaced, so their band rows keep "not yet identified". #5 is the one special case: the scorer returned NO numbers at all (maturity null / implementation null), so its M4/I8 is carried forward unverified this cycle rather than re-established; treat #5 as owing a fresh read at the next re-score, not as re-confirmed. Discharged 2026-09-01: #5 returned M4/I8 CONFIRMED on a fresh read, so that debt is paid. - 2026-09-01 FLAG carry-forward (#7, #12, #19, #20, #21, #27). Six categories returned FLAG on this re-score; none is a found regression, every one is an adversarial rejection of a proposed first-pass uplift, and each keeps its prior score under the merged-prior rule (evidence re-read at HEAD
44d9304c, clean tree, pin v1.179.0). #7 holds M4/I8 on a third consecutive rejection of 8→9: the build half of independent deployability is now per-image incremental (deploy.yml:138-161classifier,:848-873matrix), but the deploy half is still oneazure/arm-deployof the wholemain.bicep(:1115-1121) with all five images on one commit sha (:1025-1029) and a whole-fleet rollback (:1147,:1191), so its band row keeps "not yet identified". #12 holds M3/I8 on a fifth rejection of M3→4, and this is a VERIFIED hold, not an unread carry-forward: the verifier confirmed BOTH axes on byte-identical evidence (load-test.ymlunchanged since063c90dd, monthly cron:18plus dispatch:9;load-freshnesspush-only atdeploy.yml:663with its break-glass skip at:673-690;deploy.needsat:945still carrying no perf job) and re-confirmed Implementation 8 on the hot-path anchorOrdersController.cs:110. #19 holds M4/I8 on a third rejection of 8→9:IsDrawerOpenis still publicly settable (ICartStateService.cs:41) and set outside the notify path (CartDrawer.razor.cs:153), the new M88 superseded-load guard reaches only 4 of the 7 detail pages plusCatalogBrowse, and theIUiReadCacheclient cache was withdrawn 2026-08-31 (commite60bb8be) pending a per-service invalidation story. #20 holds M4/I7 on a third rejection of 7→8: still exactly 30Style=/CellStyle=occurrences across 14 razor files, zero.cssfiles changed, and the shared semantic classes consumed by 1 of the 6 MudDataGrid list pages. #21 holds M3/I8 on the placeholder-only screen-reader results log (store-ACCESSIBILITY-SCREENREADER-PASS.md:68), with the 23-scan axe suite and its chromiume2e-gate(deploy.yml:584,:594,:945,:976) re-verified. #27 holds M4/I8: no.resxor localization file has changed since9571a963(2026-08-14), and both unmet criteria are live (MoneyExtensions.cs:69-70formats withCultureInfo.InvariantCulture; the"(s)"pluralization workaround atCartDrawer.resx:20andShoppingCartList.es.resx:11). - 2026-09-04 FLAG carry-forward (#15, #20, #25, #31). Four categories returned FLAG on this re-score (the first against rubric v2, evidence re-read at HEAD
04afa39d, clean tree, pin v1.185.0); none is a found regression, every one is an adversarial rejection of a first-pass proposal, and each keeps its prior score under the merged-prior rule. #15 holds M4/I8 on a rejected 8→9: the MAUI head is still in neither solution filter (MMCA.Store.CI.slnf:24-25lists only the two web hosts) and the threeNoWarncodes atDirectory.Build.props:26still carry no rationale; the weeklymaui-audit.ymlcloses only the vulnerability-audit half and is §32 substance by its own header. #20 holds M4/I7 on a fourth rejection of 7→8: zero.cssfiles changed since44d9304c(the razor diff is the §5 folder renames) and still exactly 30Style=/CellStyle=occurrences across 14 razor files. #25 holds M4/I8 against a proposed DOWNGRADE to M3 (all three maturity legs verified live; doc drift scores on the implementation axis, the same ruling ADC received on 2026-09-01), and its band row gains a named lever for the first time (thestore-NavigationFlow.md:166menu claim versus the framework shell's/profile/sessionsand/notifications/sendroutes). #31 holds M4/I8 on a rejected 8→9 and is a VERIFIED hold, not an unread carry-forward: the verifier's structured return lost its implementation number to a schema retry cap, but its text confirmed Maturity 4 (cost-guardindeploy.needsatdeploy.yml:999, requiredsuccessat:1032) and rejected the 9 on unchanged mechanism (cost-guard.yml:7never mutates,:94prints a manual reset; one uniform tag set atinfra/main.bicep:113-119), while crediting the 2026-09-02 cost pass (0.25 vCPU right-sizing:1006, sampling:184, 300s metric export:221-222, the legacy database dropped:620-624,foundation.bicep:36-42retention cap and:88-98ACR purge) as refinement inside the 8. §31's scorecard anchors were also corrected: the formerinfra/main.bicep:80-86,271pointed at unrelated parameters.
🟠 Below-maturity-4 tracking (inclusion policy: categories scoring < 4 maturity)
These categories score maturity 3; the ledger records them per its own line-4 inclusion policy (mirrors ADC's equivalent entries).
- #19 · State Management & Data Flow · maturity 3 → 4 (weight 3). DONE (2026-07-11, remediation wave 2). The §19 fitness gate now runs in the CI.slnf arch tier:
StateManagementConventionTests(sealed subclass of the shared v1.115.0StateManagementConventionTestsBase) reflects over the three module UI assemblies (now registered asLayer.UiinStoreArchitectureMap) failing the build on any mutable static field or settable static property, plus a source scan forbidding singleton*StateService/*StateContainerregistrations. Verified non-vacuous: a seeded mutable static in Catalog.UI failed the gate with the exact offender name, then green after removal. Maturity 4 GRANTED on the 2026-07-16 re-score (two-pass, adversarially verified; the proposed impl bump to 9 was rejected as an enforcement gain, not substance). Scorecard §19 is M4/I8; moved to the protect list. - #18 · UI Architecture & Components · maturity 3 → 4 (weight 3). DONE (2026-07-11, remediation wave 2). The §18 fitness gate now runs in the CI.slnf arch tier:
UIArchitectureConventionTests(sealed subclass of the shared v1.115.0UIArchitectureConventionTestsBase) caps every*.razor.csunder Source/ at 400 lines and inline@codeblocks at 120 lines. Conformance shipped with the gate:OrderDetail.razor.cs500 → 361 (extractedOrderSummaryPanel+OrderLinesPanel) andProductDetail.razor.cs491 → 340 (extractedProductVariantsPanel), markup moved verbatim (DOM identical for the E2E selectors), all bUnit suites green. Verified non-vacuous via a seeded 402-line file. Maturity 4 GRANTED on the 2026-07-16 re-score (two-pass, adversarially verified; the proposed impl bump to 9 was rejected, impl holds 8 on the residual inline-style logic). Scorecard §18 is M4/I8; moved to the protect list. - [~] #12 · Performance & Scalability · maturity 3 → 4 (weight 2). LEVER STILL OPEN (marker corrected 2026-07-28: the
[x]contradicted this entry's own closing text and #12 sits in both ranked bands; the wave-3 work below did ship, but the maturity candidacy it recorded was declined and has been declined again since). Wave-3 delivery (2026-07-11): Both halves of the lever are now enforced deploy preconditions: (a) aload-freshnessjob indeploy.yml'sneedsfails the deploy when the latest successful monthlyload-test.ymlrun is older than 35 days (the dr-freshness pattern; latest run 2026-07-01, green), and (b) the WebVitals budgets were tightened from catastrophic-only (LCP 8000) to the Core Web Vitals "good" band (LCP 2500 / FCP 1800 / TTFB 800 / CLS 0.1 / INP 500), calibrated against measured CI maxima (LCP 172ms, 10-30x headroom), asserted inside the deploy-gating chromiume2e-gate. Maturity candidacy DECLINED on the 2026-07-16 re-score: the k6 load test itself runs monthly/on-demand, so it is capacity-planning evidence rather than a merge gate; the freshness gate bounds staleness but does not gate regressions. §12 stays M3/I8, lever OPEN: either record the monthly cadence as the accepted posture (ADC's stance) or add a latency-regression check to the merge path. Re-verified OPEN on the 2026-07-28 re-score, and both proposed moves (M3→4 and I8→9) were adversarially REJECTED: the k6 run is still monthly cron plus dispatch (re-anchored 2026-09-04: cronload-test.yml:21,workflow_dispatchat:12),deploy.needs(re-anchored 2026-09-04:deploy.yml:999) still contains no perf job, no perf fitness test exists inTests/Architecture, and the one deploy-chain hookload-freshness(re-anchored 2026-09-04:deploy.yml:714, push-only at:717) gained a break-glass skip (:729-745, skip branch at:732), which loosens rather than tightens it. Re-verified unchanged on the 2026-08-14, 2026-08-23, 2026-09-01 and 2026-09-04 re-scores (the identical M3→4 uplift was adversarially rejected each time: the workflow files backing the axis are byte-unchanged since the cycle that first rejected it, and on 2026-09-01 the verifier additionally CONFIRMED Implementation 8 on the same read, so this is a verified hold rather than a carry-forward). The 2026-07-25 performance wave is real and verified but closed defects the prior I8 already assumed absent, and two efficiency gaps stay open (the monthly k6 cadence and the full-size image blobs). The third gap this entry used to name, the sequential per-item cross-service gRPC loop it cited atBulkSetInventoryHandler.cs:40-49, is CLOSED (verified 2026-09-01): it is now a single batchedGetExistingIdsAsyncround trip atSource/Modules/Sales/MMCA.Store.Sales.Application/Inventory/UseCases/BulkSet/BulkSetInventoryHandler.cs:36-41(rationale comment:30-34), and the path this ledger cited for it,Sales.Application/InventoryItems/Commands/BulkSetInventory/, does not exist. §23 split out and RESOLVED same day (drift-analysis fold, adversarially verified): its CWV budget assertions are per-deploy enforcement independent of k6's cadence, the identical evidence ADC's twentieth cycle credited, so scorecard §23 is M4/I8 and moves to the protect list. - #13 · Observability & Operability · maturity 3 → 4 (weight 2). DONE (2026-07-11, remediation wave 6). The dashboard half already existed (the saved
store-slo-workbookAzure Monitor workbook mirrors the three SLO alerts per service); the missing runbook half landed asinfra/OPERATIONS.md: each provisioned alert (failed-requests,server-response-time,dependency-failures) mapped to concrete triage steps (workbook pane, App Insights drill path, container logs, the Stripe/gRPC/outbox failure classes) plus fast-reference recovery moves (revision rollback, PITR restore, the freshness gates) and a pair-with-sloAlertSpecsgovernance note. Split verdict on the 2026-07-16 re-score: Implementation 8 → 9 GRANTED (both prior deductions closed: workbookinfra/main.bicep:274+ runbookinfra/OPERATIONS.md), but the maturity candidacy was DECLINED: dashboards/runbooks are IaC/review-enforced, and nothing in CI fails when an alert loses its runbook pairing. §13 stays M3/I9, lever OPEN: add a CI gate asserting thesloAlertSpecs-to-OPERATIONS.mdpairing (mirrors ADC's reopened #13; one shared gate design can serve both repos). Gate SHIPPED same day (2026-07-16):Tests/Architecture/MMCA.Store.Architecture.Tests/ObservabilityConventionTests.cs(mirror of ADC's) machine-enforces the pairing in the CI.slnf arch gate: everysloAlertSpecskey needs a### ...-alert-<key>runbook section carrying the alert's current(sev N), orphans fail, 3-spec non-vacuity floor, both files embedded. Verified red on a seeded severity drift, green on the real files. Maturity 3 → 4 candidacy recorded for the next re-score. Maturity 4 GRANTED on the 2026-07-17 re-score (ObservabilityConventionTests.cs:24,34verified live in the CI.slnf arch gate,MMCA.Store.CI.slnf:53): §13 is M4/I9; moved to the protect list.
🟢 Resolved this cycle (2026-07-28, drift wave: D1/D2/D5/D6/D7 + E2/E4/E7/E8)
- [x] #29 Resilience: the DR drill was restoring a RETIRED database. The weekly
dr-drill.ymlhad no rotation and fell through to its input defaultMMCAStore, the legacy archive no app references. Every Store drill on record, scheduled and dispatched alike, hit it, soStore_Catalog/Store_Sales/Store_Identityhad never had a restore proven while the scorecard credited §29 M4/I9 on "a drilled restore" and the rubric's #1 red flag is backups that have never been restored. Fixed: ISO-week rotation across the three live DBs (MMCAStore is absent from the rotation array and can only be dispatched), drill timeout 30 -> 60 min (a run was killed at 30m16s on 2026-07-20 and recorded no proof), and ADC's break-glass (skip_freshness_gates+ mandatory justification) ported to all three freshness gates, since Store had no escape hatch at all. All three live DBs dispatch-drilled before landing: Catalog 3.6 min, Sales 2.1 min, Identity 1.3 min, all PASS. - [x] #15/#17/#34 Http2-only services had no readiness probe. Catalog and Identity run h2c-only
for cross-service gRPC, which rejects HTTP/1.1 probes, so both fell back to
tcpSocketwith no readiness probe: a replica that could not reach its database stayed in rotation serving 500s. Ported ADC's dedicated Http1 probe listener onHealthProbe:Port8081 plus the three-probe httpGet form. Verified on the deployed revisions. - [x] #31 SLO and budget alerts were notifying NOBODY.
alertEmailAddressdefaulted to''withempty()guards on every consumer, and theALERT_EMAILrepo variable was unset, so every alert routed to an action group with zero receivers. The parameter is now required anddeploy.ymlfails fast naming the missing variable. - [x] #33 README expanded from a two-line stub into a getting-started path.
- [x] ADR-055 raw-queryable rule adopted with an EMPTY
AllowedFiles: Store's Application layer had zero raw-queryable uses, so it starts with no exemptions to ratchet down. PlusDecoratorPipelineOrderTestsandHandlerResultConventionTests. - [x] Password-spray protection (D3) closed with no Store code change. MMCA.Common v1.130.0 put
the per-IP anti-spray throttle on
AuthControllerBase.LoginAsync/RegisterAsyncby default; Store inherits both actions and had never attached the attribute, so it had none. Watch the 429 rate: Blazor Server issues the login call server-side, so every Server-circuit user shares the UI host's IP against a 30/min window. Tune withAddCommonRateLimiting(authIpPermitLimit: N). - [x] E2/E4/E7/E8 adopted:
PaymentReconciliationServiceonPeriodicBackgroundService(its first adopter), the shared SQL readiness check, and the observability and graceful-shutdown test bases (118-line and factory+test files collapsed to subclasses).
🟢 Resolved this cycle (2026-07-25, performance program 2)
Second evidence-led performance pass over Common/ADC/Store. Store's share shipped as two PRs plus the v1.127.0 framework sweep.
- Output cache shared across replicas. Catalog registered
AddOutputCachewith no store while runningmaxReplicas: 2, so everyEvictByTagAsyncreached only the replica that handled the mutation: an admin repricing a product left roughly half of storefront reads serving the old value for the full 5-minute TTL, each replica filled its own copy, and a few multi-megabyte image entries per replica crowded out the product/category JSON. Redis was already provisioned and wired asIDistributedCache, so the fix wasAddStackExchangeRedisOutputCache. ADR-040's original trade-off (per-replica in-memory accepted) is superseded; see its 2026-07-25 amendment. - Order hot predicates indexed. The payment-reconciliation sweep runs every 10 minutes over
Status = PaymentInitiated AND COALESCE(LastModifiedOn, CreatedOn) < cutoffwith nothing to seek, scanning and sorting a forever-growingOrdertable to find a normally-empty set; and both order grids sortCreatedOn DESCwithOFFSET/FETCH. Added a filtered(Status, LastModifiedOn, CreatedOn)index and widenedCustomerIdto(CustomerId, CreatedOn). - Admin order search was returning an error, not results.
OrderListsent("contains", …)on anintId;IntFilterStrategyhas noCONTAINS, soFilter.Operator.NotSupportedfailed the whole query. Now an equality match when the input parses, ignored otherwise. - Warm-up tasks warmed nothing. Catalog primed a key the storefront never requests (
pageSize=10/includeChildren=Falseplus aCategories/pagedcall, versus the realpageSize=12/includeChildren=True/Categories/lookup), and the policy varies by every query parameter. Sales and Identity GET[Authorize]endpoints unauthenticated, so they 401 by design andGetStringAsyncthrew: every startup logged a warm-up failure, and neither service registers an output-cache policy at all. - Customer enrichment N+1 over HTTP.
GetByIdsAsyncfanned out one request per id, so a 25-customer admin grid page issued 25 concurrent Gateway-to-Identity round trips on every page change. Now one request via theId IN (...)operator. - Cart variant lookup projected.
GetVariantCartInfoHandlerloadedProductaggregates with an unfilterableProductVariantsinclude and discarded the surplus in memory, so a 30-variant product shipped 30 rows to render one, on the path that fires after every cart change. - Post-checkout Stripe polling backed off (banded 2s/3s/4s/6s, Stripe not called before attempt 5). A flat 3s x 20 made this page the dominant Stripe consumer under load, risking the circuit breaker for genuine session creations.
- Load and CWV coverage.
catalog-read-load.jsmeasured only guaranteed cache hits on keys the storefront never sends; it now also exercises the real storefront key and two deliberate cache-miss shapes, with hit/miss thresholds split by tag so DB-path latency has its own budget. Product detail gained a CWV budget.
Deferred from that program (record the choice)
- TD · Batch the bulk-inventory existence check. DONE (verified 2026-09-01).
BulkSetInventoryHandlerused to validate each variant with its own sequential cross-service gRPC call; it now makes ONE round trip viaproductVariantService.GetExistingIdsAsync(...)and derives the missing set by difference (Source/Modules/Sales/MMCA.Store.Sales.Application/Inventory/UseCases/BulkSet/BulkSetInventoryHandler.cs:36-41, rationale comment:30-34). Path correction: this ledger and the #12 entry citedSales.Application/InventoryItems/Commands/BulkSetInventory/, which does not exist. The whole contract path the TD said was required shipped with it:Catalog.Contracts/Protos/product_variants.proto,Catalog.Contracts/ProductVariantServiceGrpcAdapter.cs,Catalog.Service/Grpc/ProductVariantsGrpcService.cs,Catalog.Shared/Products/IProductVariantService.cs,Catalog.Shared/Products/DisabledProductVariantService.cs,Catalog.Application/Products/ProductVariantService.cs. The reuse hazard the TD named is now recorded in the code itself:GetUnitPricesAsynccould not be reused, because it drops variants whosePriceis null, so an existing-but-unpriced variant would be reported missing and fail the request. - TD · Projected order-line count. DONE (verified 2026-09-04). The persisted denormalized column the TD called the only proper fix shipped:
Order.LineCount(Source/Modules/Sales/MMCA.Store.Sales.Domain/Orders/Order.cs:53, private set, maintained by the domain at:129and documented as the count at creation, sinceCreateis the only path that adds lines), mapped inOrderConfiguration.cs:35, exposed onOrderDTO.cs:34, migrated byMMCA.Store.Migrations.SqlServer.Sales/Migrations/20260831011924_AddOrderLineCount.cs, and read by the admin grid as a realPropertyColumn(Sales.UI/Pages/Orders/OrderList.razor:109, rationale:106). No Orders list or paged query includesOrderLinesany more; the remaining includes are the money and saga paths only. Original note, for the record: the admin order grid loaded every order line only to renderOrderLines.Count, and a DTO field alone could not help because the generic query pipeline materializes entities before mapping. - TD · Product-image derivatives. Images are full-size DB blobs streamed as-is and rendered as card thumbnails with no
srcset/dimensions, so a 12-card browse grid can pull 12 full-size assets. Fixing it is a storage-design decision, not a local change. - TD · Port ADC's expand/contract migration guard. DONE (verified 2026-07-28; anchors refreshed 2026-08-23, substance confirmed unchanged). The "Expand/contract migration guard (schema rollback safety)" step runs inside the required
build-and-testjob (re-anchored 2026-09-01 by a fresh grep ofdeploy.yml: the step at:279-325, the job at:173): it fails any PR whose newly added migrationUp()body containsDropColumn/DropTable/DropIndexwithout anEXPAND-CONTRACT-OVERRIDEmarker (:317-319, the marker documented at:288), scope and rationale at:281-292, and fails closed when the base diff is unresolvable (:301-305) rather than passing vacuously. The companion model-drift gate is the step above it at:263-277. The policy is documented alongside the code atCONTRIBUTING.md:55. This is part of what lifted §8 to Implementation 9.
🟢 Resolved this cycle (2026-07-11 drift-convergence, drift plan D1-D13)
- #29 DR gates (drift plan D3).
dr-freshnessis now indeploy.needs(fails a deploy when the last successfuldr-drillis stale),dr-drill.ymlgained a weekly cron, andGracefulShutdownTests(inMMCA.Store.Gateway.Tests,MMCA.Store.CI.slnf) is CI-gated, so scorecard §29 legitimately holds M4 and lifts Implementation 8 → 9. The former honest residual (stale 2026-06-22 drill) is resolved: the weeklydr-drill.ymlcron runs green, most recently 2026-07-13, sodr-freshnessis satisfied. - #31 cost-guard deploy gate (drift plan D4).
cost-guard.ymlgained aworkflow_calltrigger and is invoked as acost-guardjob indeploy.needs, so an un-reverted surge blocks the deploy (previously weekly-cron-only). Prerequisite for a clean first gated deploy: a green latest scheduled cost-guard run. - #1 SOLID, impl 8 → 9 (drift plan D9). Scorecard §1 refreshed to cite the CI-gated
ConstructorDependencyCountTests(ceiling 7, the 8+-dependency red flag cleared) +TimeProviderinjection. - #17 DevOps, MI-SQL ACTIVATED in prod (2026-07-12; wiring landed inert 2026-07-11 as drift plan D2). The full
infra/SQL-MANAGED-IDENTITY.mdsequence is complete: Stage 1 Entra admin live on the server (SQL_AAD_ADMIN_*repo vars), Stage 2 grants verified in all three DBs (mmca-prod-apps-identityas EXTERNAL_USER +db_ownerinStore_Catalog/Store_Sales/Store_Identity), Stage 3USE_MANAGED_IDENTITY_SQL=truedeployed green (run 29192048197, full gate chain + post-deploy smoke). All three services now run passwordlessAuthentication=Active Directory Managed Identity; the SQL password path remains only as the documented dual-auth rollback. Mirrors ADC's 2026-06-28 activation; §17 Implementation 8→9 BANKED on the 2026-07-16 re-score (repo variableUSE_MANAGED_IDENTITY_SQL=true+ green run 29192048197 re-verified directly; the scorecard's stale inert claim corrected). The SQL public-network-access acceptance (no VNet) stands as documented.
✅ Already at level 4 (protect, don't regress)
Both axes satisfied (maturity 4 AND implementation >= 9), the true protect list: SOLID (#1), Design Patterns (#2), Clean Architecture (#3), DDD (#4), Data (#8), API (#9), Observability (#13), Testability (#14), DevOps (#17), Front-End Security (#26), Resilience (#29), Dependency & Supply-Chain (#32), Architecture Governance (#34). #8 joined on the 2026-07-28 re-score (Implementation 8→9): the atomic conditional-UPDATE stock decrement with deterministic lock ordering (InventoryAllocationService.cs:71), its CK_InventoryItem_AvailableQuantity_NonNegative schema backstop (InventoryItemConfiguration.cs:27), the single-transaction checkout write phase (CheckOutHandler.cs:91), the fail-closed expand/contract migration guard in the required build-and-test check (deploy.yml:279, re-anchored 2026-09-04), and the raw-IQueryable ban with an empty allowlist (RawQueryableConventionTests.cs:14 in MMCA.Store.CI.slnf:53). Protecting it means keeping the decrement atomic and the CHECK constraint in place; the honest residual is that Identity has no concurrency round-trip test.
Maturity 4 but implementation <= 8, so still ranked in the implementation band above: Vertical Slice (#5), CQRS (#6), Microservices (#7), Messaging & Integration (#10, carried at prior under rubric v2), Security (#11), Code Quality (#15), UI Architecture (#18), State Management (#19), Design System (#20), Front-End Performance (#23), Forms (#24), Navigation (#25), i18n (#27), Front-End Testing (#28), Compliance/Privacy (#30), FinOps (#31), DevEx (#33). Closing on maturity alone is exactly what let the two indices drift apart, so these stay visible rather than disappearing into the protect list. (#16 became AI-Native Application Architecture, N/A, on the 2026-09-04 rubric v2 rebase.)
Below maturity 4: #12, #21, and #22 (see the maturity band above).
History: #18 and #19 reached maturity 4 on the 2026-07-16 re-score via the CI-gated convention fitness tests; #5, #13, and #22 reached maturity 4 on the 2026-07-17 re-score; #28 reached maturity 4 on 2026-07-03 via the E2E/axe deploy gate, and #24 on 2026-07-11 via FormsConventionTests (D11); #21 Accessibility was corrected back to maturity 3 on 2026-07-11 (D6), pending a recorded screen-reader pass; #16/#20/#25/#27 reached maturity 4 in the 2026-07-02 docs re-score on gates shipped 2026-07-01; #32 and #34 reached maturity 4 on 2026-06-26. Drift watch resolved (2026-07-23 → 2026-07-28): #22's granted basis went stale on 2026-07-18 when the deploy e2e-gate was cut to chromium-only, and the predicted reopen happened on the 2026-07-28 re-score. #22 is now M3/I8, out of this list and into the maturity band; the cost trade-off behind it is recorded under Deliberate / accepted.
#34 caveat realized (2026-07-02): both governance ledgers are committed and kept current per release (§34 impl re-scored 7 → 9), and the arch fitness suite has grown to 22 test classes. Record future tactical deferrals here as
TD-NNsub-items under their category rather than in a separate file (the single-ledger model ADC adopted 2026-06-26).