Architecture governance
MMCA.ADC — Architecture Remediation Backlog
Derived from ArchitectureScorecard.md (single-axis 0-4, baseline 75%, 241/320, dated 2026-06-08). Current authoritative two-axis scores (twenty-third-cycle full re-score, 2026-07-23, pin v1.123.0): Maturity 97.2% (311/320) / Implementation 85.9% (687/800), no score moves (all 34 categories re-confirmed at their prior scores; two proposed maturity lifts, §12 and §21, adversarially rejected as verified non-moves). See the Index note for the cycle record.
Tasks are every category scoring < 4, ranked by priority = (4 − score) × weight.
Higher priority = bigger weighted gap = more index points per unit of effort.
This is the single remediation ledger. The former
TECHDEBT.mdtactical register is folded in here (2026-06-26): each deferred sub-item keeps itsTD-NNID and lives under its#NNcategory with its blocker, resolution path, and effort estimate; the recorded-but-not-scheduled choices live in the Deliberate / accepted section below. There is no separate tech-debt file (matching MMCA.Common and MMCA.Store). Effort key: S ≈ hours · M ≈ ~1 day · L ≈ multi-day.
⚠️ Index note (2026-06-27). The 75% / 241-320 figure is the 2026-06-08 single-axis baseline and is not recomputed as items below are ticked — many already-
RESOLVEDrows (#11, #14, #26, #29, #30, #32, …) have moved the real total well past it. For the current, authoritative scores use the canonical, in-repoArchitectureScorecard.md(two-axis, at framework v1.123.0: Maturity 97.2% (311/320) / Implementation 85.9% (687/800)). This backlog remains the living what-to-do-next checklist; trust the scorecard for scores. Closed 2026-06-26/27: #32 (TD-01 lock files + blocking supply-chain gates), the #14 coverage floor (TD-05), #26 (Gateway header-regression test), the #29 graceful-shutdown test (scorecard §29 impl 8→9), and #5 (slice-cohesion fitness function, scorecard §5 impl 7→8, on the v1.85.0 sweep). Closed on the v1.86.0 i18n + dark-mode sweep (2026-06-27): the #29 scheduled DR-drill gate (scorecard §29 maturity 3→4), #24 change-password client validation (scorecard §24 impl 8→9), the #20 landing-page brand-token dedupe (scorecard §20 impl 8→9), and #27 i18n flips from N/A to scored (M3/I8, ADR-027 supersedes 011). Activated 2026-06-28: managed-identity SQL DB auth in production (useManagedIdentitySql=true, scorecard §17 impl 8→9; #11 holds at 9, now capped only by the deferred public-network-access epic). The last big open lever is the E2E/axe merge gate (TD-06/07). Reconciled 2026-06-29 (re-score, pin v1.92.0): §29 was REOPENED (scorecard §29 corrected maturity 4→3: thedr-drill.ymlcron is scheduled but gates nothing, so it is Consistent/M3 not an automatic CI gate, the same standard §28 is held to), and the prior "#29 DR-drill gate closed maturity 3→4" claim above is withdrawn; #16 was also reopened (scorecard §16 is maturity 3; deleting the orphan-test folder did not by itself reach 4); §32 moved impl 8→9 (CI restore already runs--locked-modein both gating jobs,deploy.yml:40/:119); and the backlog was caught up to the scorecard by closing #6/#8/#17/#18/#20/#26/#30 (all already at maturity 4). Reconciled 2026-06-30 (enforcement-gate wave): #16/#24/#27/#29/#31 lifted maturity 3→4 by adding CI-enforced governance over already-strong implementation: #24FormsConventionTests, #27TranslationCompletenessTests, and #16FrameworkVersionConsistencyTestsrun in the CI.slnf arch gate (locally verified green, 74/74 arch tests pass); #31 cost-guard and #29 dr-freshness are wired intodeploy.needs(committed, activate on the next push). Reconciled 2026-06-30 (v1.92.0→v1.93.0 sweep, the Common tenth-wave): §5 Vertical Slice Architecture lifted maturity 3→4 (the slice-cohesion fitness functionSliceCohesionTestsis confirmed a CI merge gate inMMCA.ADC.CI.slnf), and §7 was adversarially FLAG-re-checked (a proposed impl 8→9 lift rejected) and confirmed unchanged at M4/I8. Scorecard now Maturity 94.1% / Implementation 85.9% (HEAD89d8439, pin v1.93.0); the §21 a11y axe scans were broadened 10→17 pages (impl 7→8 pending a green nightly), and the recorded screen-reader pass remains the §21 maturity lever. Reconciled 2026-07-02 (re-score, pin v1.99.0): three honest recalibrations, no code regressions. §18 UI Architecture was REOPENED (scorecard §18 maturity 4→3: no automated §18 UI-architecture fitness gate exists, so the container/presentational + code-behind conventions are review-enforced only, making §18 Consistent/M3 not Optimized/M4; its prior maturity-4 "UI convention test" basis was actually the route-authorization tests, a §25 gate). §6 impl was corrected 10→9 (the idempotent inbox covers only 2 of 4 consumer services: Conferenceappsettings.json:32, Identity:29; Engagement/Notification carry none, so real levers remain and 10 was overstated). §27 impl was corrected 8→7 (residual hard-coded English is broader than exception-path only, plus no text-expansion test). Scorecard now Maturity 93.1% (298/320) / Implementation 85.8% (686/800) (pin v1.99.0); the "Scorecard now Maturity 94.1% / Implementation 85.9%" figure above is the frozen v1.93.0 provenance. Reconciled 2026-07-03 (sixteenth-cycle full re-score, pin v1.101.0, HEADac43c8d8, all 34 categories CONFIRMED): the 2026-07-02 e2e-gate promotion is now reflected in this ledger: #28 is CLOSED (scorecard §28 maturity 4: the chromium E2E/axe suite is an enforced deploy gate,deploy.yml:303-308e2e-gatejob +:343indeploy.needs; TD-06 and TD-07 ticked), #21 re-ranked priority 6→3 (scorecard §21 M3/I8 via the same gate; the recorded SR pass remains the cheapest maturity lever), and #19 is REOPENED (scorecard §19 M3/I9: review-enforced conventions, no §19 fitness gate inTests/Architecture/). One implementation recalibration: §24 impl 9→7 (per-form error summary only on the Profile form; the six create forms surface a generic validation snackbar; raw{ex.Message}in Profile snackbars), tracked as new TD-14 under #24 (the category header stays closed: maturity holds 4 onFormsConventionTests). Scorecard now Maturity 94.1% (301/320) / Implementation 85.6% (685/800) (pin v1.101.0); the 93.1%/85.8% figures in this note are the frozen v1.99.0 provenance. Reconciled 2026-07-03 (same-day i18n completion sweep, ADR-027 Decision 9): #27's impl lever CLOSED (scorecard §27 impl 7→8: zero residual literals, dual CI gates incl. the newLocalizedTextConventionTests, MudBlazor chrome + nav localized; a new impl 8→9 sub-item tracks extending the pseudo-loc text-expansion evidence to ADC pages), and TD-14 NARROWED (raw{ex.Message}snackbars eliminated; the Profile-form gate exclusion + per-form error summaries remain). Scorecard now Maturity 94.1% (301/320) / Implementation 85.8% (686/800). Reconciled 2026-07-06 (eighteenth-cycle full re-score, pin v1.106.0, HEAD8fc9e0d2, all 34 categories CONFIRMED): every category re-confirmed at its prior score from evidence read this run (no moves). #8's TD-03 CLOSED: the optimistic-concurrency API round-trip is implemented and deploy-gated (EventDTO.cs:16carries the RowVersion token viaIConcurrencyAware,UpdateEventHandler.cs:34stamps it withSetOriginalRowVersion,OrganizerConcurrencyTests.cs:26asserts a stale token returns 409 inside the deploy-gatingMMCA.ADC.Integration.slnf); scorecard §8 holds impl 9 because the round-trip is Conference-only. #6/TD-02 partially addressed: the genuine broker round-trip test landed as the non-gating nightlyMMCA.ADC.CrossService.IntegrationTests(9 tests, Testcontainers RabbitMQ+SQL), so scorecard §6 holds impl 9; the 9→10 lever is now gating it plus enabling the inbox on all 4 consumer services. Evidence counts refreshed: arch-tests 23 classes / 25 files / 74 methods (all thin subclasses, 0 ADC-local), §14 unit 1507/223 plus integration 303 gating methods / four tiers + 9 non-gating CrossService, coverage floor 38→55.5% (actual ~57%), ADR set 001-038, §27 resx 40 base + 40 es. Scorecard indices hold Maturity 94.1% (301/320) / Implementation 85.8% (686/800). Reconciled 2026-07-10 (nineteenth-cycle full re-score, pin v1.110.0, HEAD246a24dc, all 34 categories held): every category re-confirmed at its prior score from evidence read this run (no moves, no closures, no re-ranks; the below-4 set stays §12/§13/§18/§19/§21/§22/§23/§33 with priorities recomputed byte-identical, and every TD status is unchanged: done TD-01/03/04/05/09/10, open TD-02/06/07/08/13/14). Three first-pass move proposals were adversarially rejected as verified non-moves: §12 impl 8→9 (the Notification app stays pinnedmaxReplicas: 1,infra/main.bicep:1113, even though the v1.110.0 wave provisioned Azure Managed Redis Balanced B0 and scaled the REST services tomaxReplicas: 2), §23 maturity 3→4 (the WebVitals budgets are advisory by design and no §23 fitness gate exists), and §34 impl 9→8 (no governance regression; the untracked workspace-rootArchitecturalAnalysis.mdremains the already-weighed 9-not-10 lever). Evidence refresh: ADR set 001-041, pin v1.110.0, arch tests re-run green this cycle (74/74); a contradictorymain.bicepNotification scale-pin comment (claiming no Redis backplane while the backplane key is injected at:1056) was corrected in place. Scorecard indices hold Maturity 94.1% (301/320) / Implementation 85.8% (686/800). Reconciled 2026-07-15 (twentieth-cycle full re-score, pin v1.116.0, HEAD913d088a, five scores up): the remediation-wave candidacies recorded below were adjudicated. Accepted: #18 CLOSED (scorecard §18 maturity 3→4:UIArchitectureConventionTestsin the CI.slnf arch gate), #19 CLOSED (scorecard §19 maturity 3→4:StateManagementConventionTestsin the same gate, impl held at 9 after a first-pass 9→8 proposal was adversarially rejected as unsupported), #23 CLOSED for maturity (scorecard §23 maturity 3→4: the CWV budgets became enforced assertions inside the deploy-gating chromiume2e-gateon 2026-07-11, superseding the nineteenth-cycle advisory-by-design rejection; the WASM code-split/image sub-item stays open as impl polish), #13's impl half (scorecard §13 impl 8→9 on the SLO workbook +infra/OPERATIONS.mdday-2 runbooks), and TD-14 confirmed (scorecard §24 impl 7→8). Rejected, headers corrected below: the #13 maturity 3→4 candidacy (runbooks/dashboards are review-enforced conventions and IaC, not CI-gated fitness functions, so §13 holds M3/I9 and REOPENS), the #22 maturity 3→4 candidacy (the firefox/webkit legs added to the e2e-gate runcontinue-on-error: truepere2e.yml:74, i.e. advisory inside the gate, so §22 holds M3/I8 and REOPENS), and the #33 impl 8→9 candidacy (broker parity local-RabbitMQ vs prod-Service-Bus is mitigated, not closed, perREADME.md:74, a live rubric red flag, so §33 holds M3/I8 and REOPENS). Also corrected in the scorecard: §28's false "E2E #5 un-skipped" claim (the test is re-quarantined atSpeakerSelfServiceTests.cs:57; score held M4/I8) and the §34 impl 9→8 downgrade re-rejected. Scorecard indices move to Maturity 96.6% (309/320) / Implementation 86.3% (690/800); the below-4 set narrows to §12/§13/§21/§22/§33. Reconciled 2026-07-17 (twenty-first-cycle full re-score, pin v1.117.0, HEADc4c01aa5, two scores up): the two 2026-07-16 gate candidacies were adjudicated ACCEPTED. #13 CLOSED (scorecard §13 maturity 3→4:ObservabilityConventionTestsmachine-enforces the alert-to-runbook pairing in the CI.slnf arch gate,MMCA.ADC.CI.slnf:56+deploy.yml:57,417; impl holds 9) and #22 CLOSED (scorecard §22 maturity 3→4: the deploy-gatinge2e-gatepasses all three engines,deploy.yml:309, ande2e.yml:78scopescontinue-on-errorto scheduled nightly non-chromium legs, so every invoked engine can fail a deploy; impl holds 8). Rejected: the #27 impl 8→9 pseudo-loc candidacy (PseudoLocalizationTests.cs:51covers 3 public pages of 30+, a partial extension; §27 holds M4/I8 as a verified non-move). Corrected: a stale nineteenth-cycle draft accidentally committed via PR #15 (2026-07-17) had relabeled the #12 header "RESOLVED M4/I8" and added a mislabeled "2026-07-12 twentieth-cycle" update paragraph; both are reverted below, and §12 stays M3/I8 open per the twentieth-cycle adjudication (re-confirmed this run: the k6 tier is freshness-gated viaload-freshness,deploy.yml:348,417, but executes monthly/dispatch out of band, and Notification stays pinnedmaxReplicas: 1). #33 re-confirmed M3/I8 (the 2026-07-16 Service Bus emulator tier candidacy stands recorded for a future cycle; the tier is nightly, riding the freshness gate rather than in-band). Scorecard indices move to Maturity 97.8% (313/320) / Implementation 86.3% (690/800); the below-4 set narrows to §12/§21/§33. Reconciled 2026-07-21 (twenty-second-cycle full re-score, pin v1.121.0, HEAD8509a05d, two scores down, neither a quality regression): #22 REOPENED (scorecard §22 maturity 4→3: the 2026-07-18 Actions-minute reduction cut the deploye2e-gatetobrowsers: '["chromium"]',deploy.yml:488with its rationale comment at:478-480, so firefox/webkit run only on the weeknight nightlyschedulewheree2e.yml:119keeps themcontinue-on-error; cross-engine verification is nightly-advisory again, which is M3, and the trade-off is recorded in Deliberate / accepted with its scoring cost stated plainly). §18 implementation 9→8 (the category header stays closed, maturity holds 4 on theUIArchitectureConventionTestsgate, but the largest code-behind sits flush at the enforced 400-line cap with zero headroom,HappeningNow.razor.cs:400vsUIArchitectureConventionTestsBase.cs:22, plus six files in the 360-379 band; tracked as new TD-16 under #18, effort S). Rejected for a second consecutive cycle: the #27 impl 8→9 pseudo-loc candidacy (PseudoLocalizationTests.cs:51still covers exactly 3 public pages of 36 routable pages, unchanged since the twenty-first-cycle rejection; §27 holds M4/I8). Sub-item closed: #12's deferred prod-Redis provisioning (Redis Enterprise is provisioned,infra/main.bicep:740,753,771), though the SignalR fan-out stays unexercised behind themaxReplicas: 1pin (:1424), so #12 itself stays open. Corrected: #33's load-bearingREADME.md:74quote no longer exists (the file now states the opposite atREADME.md:80-84, the Service Bus emulator tier having landed), and drifted anchors were refreshed repo-wide (CI.slnf:56→:58,deploy.yml:303-309/343/348/417→:483-489/:553/:783,e2e.yml:78→:119,main.bicep:1113→:1424,:341→:488). Scorecard indices move to Maturity 97.2% (311/320) / Implementation 85.9% (687/800); the below-4 set widens to §12/§21/§22/§33. Reconciled 2026-07-23 (twenty-third-cycle full re-score, pin v1.123.0, HEAD160f59f5, no moves): every category re-confirmed at its prior score from evidence read this run (no closures, no new items, no re-ranks, no TD changes; the below-4 set stays §12/§21/§22/§33 with priorities unchanged: #21 at 3, #12/#22/#33 at 2). Two first-pass maturity-lift proposals were adversarially rejected as verified non-moves: §12 M3→4 rejected (the k6 tier still runs monthly/dispatch out of band,load-test.yml:8, withload-freshnessa recency-only deploy check,deploy.yml:548, and Notification pinnedmaxReplicas: 1,infra/main.bicep:1424) and §21 M3→4 rejected (the recorded manual screen-reader pass is still the empty placeholder inACCESSIBILITY-SCREENREADER-PASS.md, remaining the cheapest maturity lever). The v1.122.0/v1.123.0 lockstep sweeps moved no score. Scorecard indices hold Maturity 97.2% (311/320) / Implementation 85.9% (687/800), ADR set 001-051.
Scope: 4 categories remain below maturity 4 (§12/§21/§22/§33; the 2026-07-21 twenty-second-cycle reconciliation REOPENED §22 after the 2026-07-18 CI-minute reduction cut the deploy e2e-gate to chromium only, deploy.yml:488, leaving firefox/webkit nightly-advisory, e2e.yml:119); 30 categories now score maturity 4 (protect, don't regress); none are N/A.
High-leverage fixes that each clear or relieve several items — do them once:
Rework the orphaned WebAPI integration tier→ DONE (#14): per-serviceWebApplicationFactorytiers, ~345 tests gating every deploy — also closed #11's authz-gate and #16's non-building projects, and advanced #8. SeeIntegrationTestReworkPlan.md.- Integration-coverage expansion (2026-07-06): ~74 new integration tests closed the endpoint gaps the rework left open (OAuth challenge/exchange, JWKS + OIDC discovery, DecisionSupport session-selection, Sessionize refresh, output-cache eviction, audit-stamp fidelity, RFC 9457 contract shape, GDPR export, preferences) plus explicit
[Idempotent]on Events/Sessions create; the three per-service fixtures were consolidated ontoSqlServerIntegrationTestFixtureBase. A new Notification integration project (SignalR hub + inbox) closed the last untested service. The deferred Phase 4 broker-transport tier landed asMMCA.ADC.CrossService.IntegrationTests(Testcontainers RabbitMQ + SQL, non-gating nightlycross-service-tests.yml). Deliberately skipped: dedicated rate-limit fixtures (the WAFs neutralize the limiter; proving the 300/min cap + per-IP registration throttle needs a tight-limit fixture variant, low value for the volume, revisit only if abuse is observed).→ DONE: fixed #19 (6 create forms) and #24 (6 inline-edit paths); both categories are now closed at maturity 4.UnsavedChangesGuardsweep- bUnit + axe-core harness → lifted #28 and #18 (both closed); #21 remains (the recorded screen-reader pass is its open lever).
- Doc/CLAUDE.md drift fixes → close confirmed flags in #9, #34 (and the #7 note).
- Credential hardening is one throughline across #26, #11, #17.
⚠️ Severity vs. scale. Several operational risks (#29, #12, #31) were severity-adjusted down in the audit because real conference-day load is ~76 accounts / ~67 peak concurrent. Right-size the fixes — don't over-engineer DR/scale for that volume.
🔴 Priority 6 — highest leverage
[x] #26 · Front-End Security · 2 → 4 (weight 3) · RESOLVED 2026-06-29 (scorecard §26 maturity 4 / impl 9); only the deferred TD-08 data-call proxy remains
Token handling uses two rubric-named anti-patterns, with no CSP defense-in-depth. Status (2026-06-27): cookie-only refresh + in-memory access (auth-path BFF), OAuth code-exchange, enforced CSP + hardened headers on both UI host and Gateway (now regression-gated by SecurityHeadersTests), and the 7-day refresh cookie with a recorded SameSite=Lax decision are all done. The only open piece is the deferred TD-08 full same-origin data-call proxy (access token also out of JS) + the login/register/OAuth proxy — needs interactive Aspire verification + release.
(High) JWT access AND refresh tokens persisted in JS-readable— IMPLEMENTED (cookie-only refresh; pending manual Aspire verification + release):localStoragelocalStorageis gone; the refresh token lives only in the HttpOnly cookie and is exchanged server-side (/auth/session/token+UseCookieSessionRefresh+ICookieSessionRefresherin MMCA.Common.API), and the access token is held in memory (short-lived), hydrated from the cookie via the same-origin proxy (SameOriginProxyTokenRefresher). Residual: the refresh token transits JS only during the login round-trip (to seed the cookie); the login/register/OAuth proxy that closes even that window is deferred. SeeTokenStorageDesignNote.md.(High) OAuth completion redirect carries both tokens in the URL query string— RESOLVED (Wave 1, item ①):OAuthController.CompleteAsyncnow mints a single-use code, stashes the token pair in the cache, and redirects with only?code=…; the UI redeems it viaPOST auth/oauth/exchange(OAuthController.ExchangeAsync). Tokens no longer touch the URL, history,Referer, or access logs.- (Medium)
No CSP or security headers— RESOLVED (UI host):SecurityHeadersMiddlewaresets nosniff /X-Frame-Options: DENY/ Referrer-Policy / Permissions-Policy, plus a full CSP now enforced withconnect-srcpinned to the Gateway origin (https + wss) — falls back to Report-Only only if the endpoint can't be resolved. Gateway headers now set too (2026-06-14):GatewaySecurityHeadersMiddlewareadds nosniff / X-Frame-Options / Referrer-Policy / Permissions-Policy / CSPframe-ancestors 'none'+ HSTS (prod) on every Gateway response (TD-09 — done 2026-06-14, effort S).
Fix
- [~] Move to an HttpOnly-cookie-only or BFF/token-handler model so tokens are never JS-readable. → implemented as the auth-path BFF (C+ proper): cookie-only refresh + in-memory access. Full data-call proxy (access also out of JS) deferred; login/register/OAuth proxy (closes the login-flash) deferred. Pending manual Aspire verification + release. Deferred pieces tracked as TD-08 (effort L): build the same-origin data-call proxy + proxy the login/register/OAuth flows, verify on the Aspire stack interactively, then release. See
TokenStorageDesignNote.md. - Replace the token-bearing OAuth redirect with a one-time authorization code exchanged via POST. → done (①):
OAuthCodeExchangeRequest+auth/oauth/exchange; covered byOAuthControllerTests(success, replay-burn, missing/expired, empty-code). - Add a CSP + standard security headers on the UI host and the Gateway. → DONE (both): UI host CSP enforced with
connect-srcpinned (BlazorCspPolicyProvider); the Gateway sets the hardened headers on every response via the sharedAddCommonSecurityHeaders/UseCommonSecurityHeadersmiddleware registered first in its pipeline (Source/Hosts/MMCA.ADC.Gateway/Program.cs:31,61). (The line-31 audit note above mentioned a bespokeGatewaySecurityHeadersMiddleware; the shipped implementation is the shared Common middleware — same headers, one source.) - Add an integration/E2E test asserting header presence so it can't regress. → DONE (2026-06-27):
MMCA.ADC.Gateway.Tests/SecurityHeadersTestsboots the real Gateway viaWebApplicationFactory<Program>(no SQL — runs in the fast CI tier /CI.slnf) and asserts/alivecarriesX-Content-Type-Options: nosniff,X-Frame-Options: DENY,Referrer-Policy,Permissions-Policy, CSPframe-ancestors 'none', and HSTS (Production env). A refactor droppingUseCommonSecurityHeaders()now fails CI. - Shorten the 30-day refresh cookie; consider
SameSite=Strict. → DONE (2026-06-27, with recordedSameSitedecision): the session/refresh cookie is already 7 days (not 30) —SessionCookieJar(MMCA.Common.API) pinsLifetime = TimeSpan.FromDays(7), "aligned to the refresh-token lifetime so a cookie never outlives the credential it carries."SameSite=Strictis deliberately NOT adopted:SameSite=Laxis load-bearing for the SSR-prerender path ([Authorize]pages opened in a new tab / on F5 / following an external link are cross-site top-level navigations that Strict would strip the cookie from, forcing a spurious /login bounce — the exact scenario ADR-022's cookie scheme exists to serve); CSRF is covered defense-in-depth by the/auth/session/tokenendpoint'sSec-Fetch-Sitecheck + POST-only +SameSite=Lax.
[x] #28 · Front-End Testing & Quality · 3 → 4 (weight 3) · RESOLVED 2026-07-02, reconciled here 2026-07-03 (scorecard §28 maturity 4 / impl 8): the chromium E2E/axe suite is an enforced deploy gate (e2e-gate in deploy.needs, deploy.yml:303-308,:343; e2e.yml:31 workflow_call), closing TD-06 and TD-07. Firefox/webkit stay advisory nightly (#22); visual-regression snapshots remain optional polish
Only one UI test level exists (manual, non-gated E2E).
- (Medium) UI E2E suite excluded from CI — no front-end merge gate.
deploy.yml:40-48runs onlyCI.slnf; E2E needs the full Aspire stack and is run manually, so UI regressions can merge to prod undetected. - (Medium) Accessibility untested — no axe/Lighthouse anywhere.
- (Low) No bUnit/component tests for ~45 Blazor components.
Fix
- Add a bUnit component-test project (conditional rendering / edge states). → DONE (3 module projects):
MMCA.ADC.Conference.UI.Tests(bUnit v2 harness — MudServices + loose JSInterop + permissive-auth doubles so<AuthorizeView>renders), inCI.slnf, covering the three public detail pages (Event/Speaker/Session — loaded vs not-found) plus the Session page's<AuthorizeView>action bar (hidden anonymous / shown authenticated);Identity.UI.Tests(a mutable-auth harness, since Identity pages injectAuthenticationStateProviderdirectly) —Profileloaded/error-state bUnit tests + the/usersauthz fitness test; andEngagement.UI.Testscovering both feedback forms —EventFeedbackTests(dynamic question render by type + per-question upsert skipping unanswered) and nowSessionFeedbackTests(2026-06-27) — precondition gating (BR-16 unscheduled / BR-91 service / BR-49 status block the form), session-not-found error state, question render by type, and upsert-only-answered. List pages deliberately skipped for bUnit —DataGridListPageBaseis infra-heavy (7 injected services + JS interop/PersistentComponentState); its plumbing belongs to MMCA.Common's own tests, the derived page logic is thin. - Add a route-authorization fitness test —
ManagementRouteAuthorizationTests(reflection over Conference.UI): admin-namespace pages must keep[Authorize(Roles="Organizer")], the set is asserted non-empty (no vacuous pass), and public pages must stay anonymous at the page level. Closes the #25 residual. - Wire axe-core (
Deque.AxeCore.Playwright) + ≥1 a11y assertion (TD-06) → DONE (2026-07-02, ticked on the 2026-07-03 reconciliation): the axe-coreAccessibilityTests(17 pages,Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/AccessibilityTests.cs) run inside the deploy-gating chromiume2e-gatejob (e2e.yml:236runs the whole E2E project;deploy.yml:343puts e2e-gate indeploy.needs), so the a11y assertions now gate every deploy. - Make a smoke E2E subset an automatic merge gate (TD-07) → DONE (2026-07-02, exceeded): the full chromium suite (not just a smoke subset) is the deploy-gating
e2e-gatejob (deploy.yml:303-308uses: ./.github/workflows/e2e.ymlwithbrowsers='["chromium"]';e2e.yml:31workflow_call), promoted after validation run 28604877733 (first fully green three-browser matrix). The former Blazor-Server-under-load blocker was resolved by theE2E_FORCE_SERVERpin + reload-and-rewait fixes (see the 2026-07-02 notes below). - Add Playwright visual-regression snapshots for key pages.
E2E merge-gate status (nightly watch) — updated 2026-06-20: the Playwright suite is still red → not promotable to a merge gate. Latest nightly (run 27865189736, main, 08:08 UTC): chromium 10 failed / 83 passed / 93 total (all 10 failed through 3 retries); firefox + webkit also red (advisory, continue-on-error). Breakdown: most are the documented residual cold-start/contention failures — TimeoutException on the 60s auth wait + InvalidOperationException: Registration failed (Blazor Server-mode contention on the 2-core runner, proven CI-only). One genuine defect has now been FIXED + CI-VERIFIED: OrganizerEventManagementTests.PublishEvent_ShouldShowPublishedStatus was a strict-mode violation — page-wide GetByText("Published") matched 3 elements (the row label, the status chip, and the "Event published." snackbar, all substring + case-insensitive). Now scoped to the status chip via a new EventDetailPage.StatusChip locator (DetailTable .mud-chip) + ToContainTextAsync; the symmetric UnpublishEvent GetByText("Draft") was hardened the same way. Verification — branch fix/e2e-publishevent-selector, run 27872057609 (2026-06-20): chromium 8 failed / 85 passed (down from 10); PublishEvent/UnpublishEvent now pass (0 occurrences in the failure log). Residual cluster = all 8 remaining failures are the register-helper contention path (RegisterNewUserAsync → "Registration failed: One or more errors occurred") in MMCA.Common.Testing.E2E. The Identity service log proves the backend registrations succeed (≈10 UserRegistered events, zero errors), so this is a UI-side success-detection race in Server-interactive mode before WASM hydrates — not a product bug. This run had no auth-timeout or logout failures (passed on retry), so the residuals are contention-variable but centered on the Common register helper; fixing them is a Common change + release + sweep. Update 2026-06-20: that Common fix was attempted (v1.72.0 — force WASM interactivity before auth submit) and REVERTED — forcing the page onto WASM broke login in the CI E2E env (WASM-mode auth fails there; the prerendered Server-mode path was the only working one), stalling the suite into the 50-min job cap with ~zero progress. The 8 register/login reds are now accepted as documented non-gating CI contention flakes (E2E is off the deploy path; the suite otherwise completes). If revisited, use a seeded-account / reduced-register-load approach — not WASM forcing. No wave item unblocks yet — the merge-gate task above and #22 cross-browser pass remain blocked until the matrix is green across engines.
E2E ROOT CAUSE FOUND (2026-06-29) — definitive, Playwright-trace-proven. The gate stays advisory by deliberate decision; the blocker is a Blazor-Server-under-load limit, not a fixable test/app bug. A full self-hosted-runner investigation was run to escape the 2-core GitHub-hosted ceiling, and it ended by pinning the actual cause. What was tried and ruled out, in order: (1) 2-core GitHub-hosted baseline is 84/93 (≈29 first-pass fails, retries recover ≈20; the --retry-failed-tests-max-percentage 40 cap is load-bearing). (2) A Windows dev-box self-hosted runner is not viable — three distinct blockers: shell: bash resolves to WSL (no /bin/bash), the runner collides with a concurrent local Aspire session, and DCP cannot allocate container ports against Windows' reserved/Hyper-V port ranges. (3) A dedicated Azure Linux VM (adc-e2e-runner, D4as_v5 then D8ads_v5 8-core + NVMe, Docker, runner adc-e2e-linux) runs the unmodified ubuntu workflow and the build/stack come up cleanly — but the suite fails worse than GitHub-hosted (32–63 first-pass fails vs ≈29). The faster the host, the more it fails. Diagnosis chain: every test passes in isolation (simple Category create AND complex Event create with date-pickers/timezone) and a 7-test batch passes 7/7; only the full 93 fails, on both the console runner and dotnet test/MTP (so not the runner, not parallelism — all in one serial E2E collection). Slowing the pace halved the failures (a trace-instrumented run was 32 vs 59). Per-test Playwright traces (Common v1.90.0 added per-failed-test capture) are conclusive: every failure (Login, Register, CreateRoom, CreateSession, ...) shows the same reconnect / WebSocket / blazor-error signature at the 15s timeout. Root cause = Blazor Server SignalR circuits drop under sustained fast-suite load. Each test uses a fresh browser context (no cached WASM) so every test runs in Server mode with a live circuit; under the fast pace the UI host is CPU-saturated by the circuit churn, the keepalive heartbeat stalls past the client timeout, the WebSocket drops, the page sits in the reconnect overlay, and the next fill/click times out. The slow GitHub runner's pace is what keeps circuits stable → 84/93. Fixes attempted and rejected (do NOT repeat): a symmetric ClickAndVerifyAsync re-click helper (Common v1.89.0) — no effect (clicks register; the form is fine); GotoProtectedAsync full-page-load nav — no effect; config-gated DisconnectedCircuitRetentionPeriod/MaxRetained shrink — no effect (memory knob, not the CPU bottleneck); config-gated SignalR ClientTimeoutInterval 120s — no effect (circuits are actively closed, not merely timing out). Conclusion: a fast-runner gate needs either a deliberately slow pace (i.e. GitHub-hosted, which already gives 84/93) or dedicated per-service CPU (a real infra spend) — both disproportionate to this category. Decision (2026-06-29): keep the GitHub-hosted nightly advisory; the self-hosted experiment was fully reverted (e2e.yml back to ubuntu-latest + full matrix; the experimental page-object / GotoProtected / UI-host circuit changes reverted to the exact 84/93 code; the Azure VM + runner deleted). The Common helpers shipped along the way (ClickAndVerifyAsync v1.89.0, per-test trace capture v1.90.0) stay released and additive. If anyone resumes TD-07: start from the per-test trace evidence above; the only paths that can work are reducing the suite's request pace on a fast host or giving the UI host dedicated CPU — not another test-side or circuit-config tweak.
Forced-WASM follow-up, CI outcome (2026-07-02): REVERTED for CI, kept for local. The v1.92.0 sweep (6b1239b) tried to eliminate the Server circuits entirely by forcing WebAssembly render mode under E2E (E2E_FORCE_WASM → AppHost → E2E:ForceWebAssembly → App.razor), validated on a fast local box (it even surfaced and fixed real per-test issues: the RenameCategory persisted-filter bug, the speaker-dashboard stale cache). Its first CI execution (run 28560329396, 2026-07-02; the two intervening nightlies never reached the tests: a GitHub Actions billing lapse on 06-30 and the Microsoft.OpenApi NU1903 advisory on 07-01, both since resolved) failed wholesale: 0 passed / 24 uniform ~110s timeouts in 44 min, job killed at the 50-min cap. Evidence from the run: prerendered pages render fine (web-vitals JSONs captured, LCP ≈ 200-400ms), the backend is healthy (warm-up POST /Auth/login → 200), but no interactive flow ever completes: no navigation, no logout button, and no error alert either: clicks land on a dead prerendered DOM. On the 2-core hosted runner every fresh browser context pays a cold WASM runtime boot while the whole stack shares the same cores, and WaitForBlazorAsync's readiness probe (window.Blazor?._internal) is satisfied during prerender, so the suite interacts before WASM interactivity exists. Same outcome as the 2026-06-20 v1.72.0 attempt above ("WASM-mode auth fails in the CI E2E env… stalling the suite into the 50-min job cap"), now with the mechanism identified. Decision: e2e.yml no longer sets E2E_FORCE_WASM (back to the InteractiveAuto 84/93 Server-mode baseline); the BR-213 registration-throttle lift is preserved via a new independent E2E_LIFT_REGISTRATION_THROTTLE AppHost gate; per-failed-test Playwright traces now ride the CI artifact (E2E_TRACE=artifacts/traces/) so any future red nightly is diagnosable offline (this run had no traces; the env var was never set in CI). E2E_FORCE_WASM remains supported for local fast-box runs, where WASM mode works. If anyone retries WASM in CI, it needs all three of: (a) a WASM-aware readiness signal (a marker rendered only by interactive code, not Blazor._internal), (b) amortizing the per-context WASM boot (fresh Playwright contexts have no shared cache, e.g. serve the _framework bundle from a shared route-cache), and (c) a raised job cap; any one alone repeats this failure.
Residual-9 trace triage (2026-07-02, run 28589825631: 89/99, the first run with per-failed-test traces) and the InteractiveAuto discovery. All eight timeout failures share ONE frame: the post-login WaitForBlazorAsync inside E2ETestBase.LoginAsync, and seven of eight are LoginAsUserAsync (the attendee cluster, late-suite). The traces overturn the "pure Server-mode circuit drop" reading for this cluster: the network capture shows a _framework/*.wasm download storm mid-test (520 requests in one trace) because InteractiveAuto switches each test's SECOND page load (the post-login forceLoad of "/") to the background-downloaded WASM bundle, whose .NET runtime boot under 2-core contention exceeds every wait; the bundle download itself also starves the live Server circuits (the login click's 60s three-way auth-wait timeout). A second latent bug: Playwright's timeout exception derives from System.TimeoutException, NOT PlaywrightException, so LoginAsync's catch-and-rewait never actually caught it (the built-in "retry" never ran). The ninth failure (Speaker_EditOwnProfile, BR-207) burned its 8 re-login attempts on the same contended UI login path instead of measuring event propagation. FIXES (2026-07-02): e2e.yml pins E2E_FORCE_SERVER=true (App.razor three-way mode: CI pins Server, E2E_FORCE_WASM stays local-optional, prod stays InteractiveAuto); Common E2ETestBase post-auth wait now catches both exception types and RELOADS once before re-waiting (fresh request, HTTP-cached assets) instead of watching the same stalled boot; LoginAsLinkedSpeakerAsync polls POST /Auth/login via the API for the speaker_id claim and performs a single UI login only after propagation lands. Target: chromium at or above 97/99 over a 3-nightly soak, then promote chromium E2E to a merge gate (the standing #28 exit criterion). Full plan: workspace Docs/Planning/E2E-RemainingFlakes-plan.md.
MERGE-GATE PROMOTED (2026-07-02, user-directed ahead of the soak). Validation run 28604877733 on the full fix stack returned the first fully green three-browser matrix ever (chromium 99 tests / 0 failed / 1 retry; firefox and webkit green outright), and the gate was promoted immediately: e2e.yml gained a workflow_call entry point with a browsers input (dispatch/nightly keep the full matrix), and deploy.yml now has an e2e-gate job (uses: ./.github/workflows/e2e.yml with browsers='["chromium"]') in deploy.needs alongside cost-guard and dr-freshness. A red chromium suite now blocks the production deploy; firefox/webkit stay advisory on the nightly. Deploy latency cost: one chromium E2E job (roughly 40 minutes) per deploy. The 3-nightly soak still runs as confirmation; if a genuine contention flake blocks a deploy, re-run the job after reading its trace artifact, do not demote the gate on a single red. This is the #28 maturity 3-to-4 lever (E2E is now an enforced deploy gate, not nightly-only); the next re-score should re-evaluate #28 and the #22 cross-browser item (green firefox/webkit matrix).
[x] #29 · Resilience, Reliability & Business Continuity · 3 → 4 (weight 3) · RESOLVED 2026-06-30 (scorecard §29 maturity 4 / impl 9): a dr-freshness job in deploy.needs now gates the deploy on a recent successful DR drill, so the recovery proof is enforced by a CI gate (committed, activates on the next push)
Strong in-app resilience (Polly, SQL retry, outbox, health probes), now with a first-class recovery story. (Flags severity-adjusted low for scale — but collectively they drive the score.) Status (2026-06-27): RTO/RPO note, LTR + executed restore drill, SLO alerts/workbook, and the fault-injection + graceful-shutdown tests are all done — the graceful-shutdown half was CI-verified (GracefulShutdownTests), which lifted scorecard §29 impl 8→9. Correction (2026-06-29 re-score): the v1.86.0 claim that the scheduled DR-drill closed the maturity-4 lever was reversed. dr-drill.yml:27-29 is a weekly cron that gates nothing (absent from deploy.yml:284's needs; CLAUDE.md:251 buckets it among the non-deploying operational workflows), so it is Consistent/M3, not an automatic CI gate (the same standard §28 is held to). Scorecard §29 is maturity 3 / impl 9. The open maturity-4 lever is to make the recovery proof actually block a merge/deploy; the conference-day minReplicas:2 choice stays a deliberate accepted-risk deferral.
- Undefined RTO/RPO anywhere in repo/infra/docs.
- Untested DB restore; Basic-tier 7-day PITR default, no LTR.
deploy.yml:258-289,infra/main.bicep:207-222. - SPOFs without documented risk acceptance: one SQL server (publicNetworkAccess Enabled), one Container App Environment, all apps
minReplicas:1.infra/main.bicep:149-159,270,…. No failure/chaos testing; graceful shutdown unverified.— RESOLVED: fault-injection (Common) + Gateway graceful-shutdown test (see fix item below).- No reliability targets/alerting — App Insights wired but no metric alerts/action groups.
infra/main.bicep:127-147.
Fix (right-sized for the real load)
- Write down RTO/RPO + a single-region risk-acceptance note —
infra/DISASTER-RECOVERY.md(targets table, accepted SPOFs, backup posture, recovery runbook). - Enable LTR/geo-redundant backups and run one restore drill — LTR (P4W/P12M/P1Y) added on all four live
ADC_*DBs (serviceDatabaseLtrinmain.bicep); PITR is already geo-redundant (Basic default). Restore drill automated (one-clickdr-drill.yml+scripts/dr-restore-drill.ps1) and executed end-to-end 2026-06-20: PITR restore ofADC_Conferenceinto a throwaway copy in 2.6 min (vs 2 h RTO), verified Online, cleaned up; row recorded inDISASTER-RECOVERY.md. §29 residuals (TD-10, effort S) — DONE 2026-06-20: the fault-injection test (ResilienceCircuitBreakerFaultInjectionTests+ outbox broker-degrade, in MMCA.Common), the automated/executed restore drill, and the Azure Monitor SLO workbook (sloWorkbookinmain.bicep←workbooks/adc-slo-workbook.json) all landed. - Make the restore drill an actual merge/deploy gate (maturity-4 lever) → DONE 2026-06-30: a lightweight
dr-freshnessjob was added todeploy.ymland to thedeployjob'sneeds. It fails the deploy unless the latestdr-drill.ymlrun concludedsuccesswithin an 8-day freshness window (covering the weeklycron: '0 6 * * 1'), via onegh apiActions read, so the recovery proof now blocks the deploy with no per-deploy restore cost (right-sized for the ~67-peak load). The real PITR restore still runs ondr-drill.yml's weekly cron;GracefulShutdownTestsremains CI-gated, so impl holds at 9. Committed; activates on the next push. Effort S. - Add metric + log-query alerts with an action group for key SLOs —
main.bicepnow provisions an action group + three App-Insights metric alerts (failed requests, server response time, dependency failures), email via theALERT_EMAILrepo variable. -
ConsiderDeliberately deferred — 2026 load (~76 acct) didn't warrant it; recorded as accepted risk in DISASTER-RECOVERY.md.minReplicas:2for the gateway/UI on conference day only. - Add a basic fault-injection / graceful-shutdown test. → DONE (2026-06-27): the fault-injection half was already covered by
ResilienceCircuitBreakerFaultInjectionTests+ the outbox broker-degrade test in MMCA.Common (TD-10). The graceful-shutdown half now lands asMMCA.ADC.Gateway.Tests/GracefulShutdownTests— it boots the real Gateway host viaWebApplicationFactory<Program>, requests a stop under a bounded 20s token, and assertsIHost.StopAsyncdrains and completes (the host reachesApplicationStopping→ApplicationStopped) within the timeout; a hosted service that refused to drain would cancel the token and fail the test. Headless, inCI.slnf.
🟠 Priority 4
[x] #30 · Compliance, Privacy & Data Governance · 2 → 4 (weight 2) · ⚖️ was legally urgent · RESOLVED 2026-06-29 (scorecard §30 maturity 4 / impl 9); cross-service export aggregation is the only residual
The (4−score)×weight formula puts this at 4, but the High flag is a contractual/regulatory exposure that contradicts a shipped, publicly-served policy — treat it as do-soon.
(High) Soft-delete is the only deletion path for PII —— RESOLVED:User.Delete()retains email, name, password hash/salt, device metadata, OAuth keys indefinitely.Usernow implements the frameworkIAnonymizableseam (v1.53.0);User.Anonymize()irreversibly overwrites email (→ uniquedeleted-{id}@anonymized.invalid), name, password hash/salt, device metadata, OAuth keys, and revokes the refresh token, idempotently, keeping the row for FK/audit (anonymize-in-place, ADR-005).DeleteUserHandlercalls it on every deletion request, so erasure is immediate — well inside thePRIVACY.md§5 "30 days" promise. Covered byUserAnonymizeTests(3 domain tests);DeleteUserHandlerTestsgreen.(Medium) PII (email + first/last name) written to App Insights traces with no redaction.— RESOLVED: the four PII-bearingUserRegisteredHandler.cs:179-198.LoggerMessagetemplates (email ×3, name ×1) now log only the stable{UserId}/counts — no email or name reaches the trace pipeline (matchesPRIVACY.md§1.2's stated log scope).(Medium) Data-subject access/export is manual-email-only; only deletion has an endpoint.— RESOLVED (Identity-owned data):GET /users/{userId}/export(owner or Organizer) returns a portableUserDataExportDTO(email, name, role, login provider, device metadata, speaker link, timestamps), excluding credentials (hash/salt, refresh token, provider key). Covered byExportUserDataHandlerTests. Cross-service aggregation (Engagement bookmarks, Notification messages) for full §7 coverage remains.
Fix
- Implement a real erasure path —
IAnonymizable+ anonymize-on-delete (immediate erasure). (A scheduled-purge backstop for rows soft-deleted by other paths is optional now that delete erases inline.) - Redact/tokenize PII before logging — done in
UserRegisteredHandler. - Add an export/access endpoint:
GET /users/{userId}/export(Identity-owned data);cross-service bookmark/notification aggregation is the remaining piece→ cross-service aggregation DONE 2026-07-11 (remediation wave 6): the export now aggregates Engagement (session bookmarks + submitted live-Q&A questions, newuser_engagement_export.protorpc mirroring the bookmark-count pattern) and Notification (inbox items, newuser_notification_export.protorpc on the existing ADR-012 grpc ingress; a newNotification.Sharedlayer carries the seam per module-isolation rules). Aggregation is best-effort per section (Available=false+ empty lists when a peer is down after the Polly pipeline; the export never fails on a peer outage). Identity gains gRPC edges to both peers (AppHostWithReferencewithout deadlockingWaitFor; bicep env mirroring the existing gRPC-edge mechanism). 9 handler unit tests + a payload-shape integration test (faked peers). Recorded follow-up, deliberately out of scope: event/session feedback answers live in the Conference DB (EventQuestionAnswer/SessionQuestionAnswer), so full-corpus export would need a third (Conference) edge; the recorded §30 residual named only bookmarks + notifications, both now covered. §30 Implementation 9→10 candidacy recorded for the next re-score. - Add a fitness/integration test proving an erasure path exists and that PII is not logged — domain unit tests added; the end-to-end erasure + no-PII-in-logs assertion rides the #14 integration-tier rework. SHIPPED 2026-07-16:
ErasureAndPiiLoggingTests(Identity integration tier, deploy-gating): (1) a deleted account is erased from every API surface end to end (login 401, export 404, listing clean) through the real host pipeline; (2) a full register-login-delete lifecycle emits ZERO log lines carrying the account's email or names (every host log line captured via the newPiiLogCapturesink in the test factory, asserted against unique markers). §30 I9→10 candidacy already recorded stands on stronger evidence.
[x] #27 · Internationalization · 3 → 4 (weight 1) · RESOLVED 2026-06-30, scorecard §27 maturity 4 / impl 8 as of the 2026-07-03 i18n completion sweep: dual CI gates (TranslationCompletenessTests floor 40 + the new LocalizedTextConventionTests), zero residual hard-coded literals (titles/snackbars/breadcrumbs/nav/home), MudBlazor chrome localized via the inherited ResxMudLocalizer, ErrorMessages.Success concatenation eliminated (obsoleted upstream, 28 sites swept). The impl 8→9 lever is extending the pseudo-loc no-overflow (text-expansion) E2E evidence, which today covers only the shared chrome in Common's gallery gate, to ADC's own pages
(Low) Hardcoded user-facing English throughout markup, e.g.RESOLVED (v1.86.0 sweep, 2026-06-27): ADC now ships real en-US + es i18n (36 baseSource/Modules/Conference/.../Pages/Speaker/SpeakerDashboard.razor:7-60; no.resx, noIStringLocalizer,InvariantCulturedisplay..resx+ 35.es.resxacross the three module UIs + three API error-resource sets,IStringLocalizer<T>in ~33 pages, culture-aligned SSR/Server/WASM, cross-deviceUser.PreferredCulturepersistence, backend error localization keyed onError.Code,SupportedCultures = [en-US, es]). The scorecard flips §27 from N/A to scored at Maturity 3 / Implementation 8. ADR-011 (single-locale) is superseded by ADR-027.
Fix (weight 1)
-
Cheapest: record an ADR/note that single-locale is intentional.→ SUPERSEDED: ADR-011 is now superseded by ADR-027 (multi-locale i18n, canonical in MMCA.Common) (en-US + es); the prior single-locale stance no longer holds. - Externalize strings to resources + register
AddLocalization/RequestLocalization+ culture-aware date formatting — done on the v1.86.0 sweep (evidence above); was formerly the conditional "only if multi-locale is ever needed" item. - (maturity-4 lever) Add an i18n translation-completeness CI gate → DONE 2026-06-30:
Tests/Architecture/MMCA.ADC.Architecture.Tests/TranslationCompletenessTests.cspairs every base.resxunderSource/with an.es.resxsibling and asserts identical key sets (36/36 today; runs in the CI.slnf arch gate). The residual code-behind English (Profile.razor.cs:38,43,101,105+EventCreate.razor.cs:60) was externalized to resources this wave. Lifted scorecard §27 maturity 3→4.- Remaining impl-7 polish → DONE 2026-07-03 (i18n completion sweep, scorecard §27 impl 7→8): MudBlazor built-in text localizes via the framework's
ResxMudLocalizer(inherited on the sweep); all residual snackbars, page titles, breadcrumbs, nav items, and both ADCHome hosts externalized (~260 new en+es key pairs across 68 resx pairs); the newLocalizedTextConventionTestsgate prevents regression; the text-expansion evidence ships upstream (Common's gallery pseudo-loc no-overflow gate covers the shared chrome). - (impl 8→9 lever) DONE 2026-07-11 (remediation wave 6):
Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/PseudoLocalizationTests.csextends the pseudo-loc evidence to ADC's own public pages (/,/conference/events,/conference/sessions): activatesqps-Plocvia the app's own/culture/setendpoint (cookie-based, because the InteractiveServer circuit's culture rides the SignalR handshake cookies, not the page query string), asserts the[!!sentinel renders without an en-US leak on a per-page resx-owned probe, and applies Common's exact no-horizontal-overflow assertion; a default-culture companion test guards the probes against drift. No host/AppHost change was needed (the culture endpoint + Development-only pseudo locale were already wired). Rides the deploy-gating chromium e2e-gate; first genuine run in CI. §27 Implementation 8→9 candidacy recorded for the next re-score. Adjudicated 2026-07-17 (twenty-first cycle): REJECTED as a partial extension (PseudoLocalizationTests.cs:51covers 3 public pages of 30+ routable pages), so scorecard §27 holds M4/I8, a verified non-move. Broadening the pseudo-loc tier across the authenticated surfaces is the open 8→9 lever.
- Remaining impl-7 polish → DONE 2026-07-03 (i18n completion sweep, scorecard §27 impl 7→8): MudBlazor built-in text localizes via the framework's
🟡 Priority 3 — score 3, weight 3 (one rung from a 4)
[x] #14 · Testability & Test Strategy — 3 → 4 · RESOLVED (see IntegrationTestReworkPlan.md)
(High) The 258-test Testcontainers integration tier references the deleted— RESOLVED: reworked as per-serviceMMCA.ADC.WebAPIhost, won't build, and is excluded.WebApplicationFactory<Program>tiers (Identity/Conference/Engagement, ~345 tests) over a SQL-service CI container, plus the revivedMMCA.Common.APImiddleware unit tests. In-process JWT override (forAddForwardedJwtBearer), gRPC fakes, broker InProcess short-circuit, Respawn reset. Runs viaMMCA.ADC.Integration.slnfand gates every deploy (integration-testsjob indeploy.yml, a required dep ofdeploy). All CI-verified green.
Fix
- Rework integration tests against the new per-service hosts and re-include them.
- Wire coverage collection (TD-05 — done 2026-06-26): coverage is collected via
dotnet-coverage(cobertura) and gated by a 55.5% unit-tier line-coverage floor (ADC's own+MMCA.ADC.*;-*.Testscode, ratcheted to 55.5 after the 2026-07 coverage program, actual ~57%) that hard-fails the deploy-gatingbuild-and-testPR job (deploy.yml:83). No longer report-only. - Cross-service handler coverage (Phase 4 headline flows) — the consumer-side logic is now re-homed as in-process integration tests on the per-service fixtures (resolve the real
IIntegrationEventHandler<T>from the booted host, assert against the real DB; runtime-gated by the SQLintegration-testsjob):Conference.IntegrationTests/CrossService/CrossServiceUserRegisteredTests.cs(BR-207 name-match auto-link / ambiguous-skip / no-match-skip) +Identity.IntegrationTests/CrossService/CrossServiceSpeakerLinkTests.cs(SpeakerLinkedToUser/SpeakerUnlinkedFromUserset/clearUser.LinkedSpeakerId). Pairs withOutboxFidelityTests(which covered the producer side only). Added via a small additiveServicesaccessor on both fixtures; compile 0/0. - [~] Phase 4 broker-transport tier (TD-02), landed 2026-07-06 as a non-gating nightly: the genuine MassTransit broker round-trip (Testcontainers RabbitMQ + dual-host transport/outbox fidelity, not just handler logic) now runs as
MMCA.ADC.CrossService.IntegrationTests(9 tests) oncross-service-tests.yml. Optional remaining coverage: speaker analytics and the Conference→Engagement bookmark-count gRPC reads. Making the tier a deploy gate is the shared §6 impl 9→10 lever (see TD-02 under #6).
[x] #11 · Security — 3 → 4 · RESOLVED
(Medium) Rate limiter is inert — named policies but no— RESOLVED: MMCA.Common 1.54.0'sGlobalLimiter/[EnableRateLimiting].AddCommonRateLimitingnow attaches aGlobalLimiter(429 over 300 req/min per authenticated user; partition name→user_id→IP). Anonymous traffic is deliberately unlimited (public endpoints output-cached, login has its own protection, and Blazor-Server anonymous traffic shares the UI host IP); health//alive/JWKS/application/grpcbypassed. Swept to all 7 services (ADC + Store) on the 1.54.0 bump; CLAUDE.md "100 req/min" claims corrected.(Medium) No automated server-side authorization gate.— RESOLVED: the #14 per-service tier includes the access-denied authz matrices (anonymous→401, attendee→403 across all services, ~55 tests), gating every deploy.- (Medium) Prod secrets in Container App secrets + ACR admin password — not a vault/managed identity.
Fix
- Attach a global limiter (Common change; corrected CLAUDE.md's "100 req/min" claim) — DONE (MMCA.Common 1.54.0, 300/min per authenticated user, swept to ADC + Store).
- Add API-level authz integration tests — done via the #14 access-denied split.
- Move secrets to Key Vault + managed identity (pairs with #17). → DONE: ACR pull via shared UAMI (AcrPull); all runtime secrets (SQL/Service Bus conn strings, RSA/JWT keys, SMTP/OAuth/Anthropic) now in RBAC Key Vault
adckv<token>, read by the apps viakeyVaultUrl+ the same UAMI (Key Vault Secrets User). No plaintext Container App secrets remain.
[x] #19 · State Management & Data Flow · 3 → 4 (weight 3) · RESOLVED 2026-07-15 (twentieth-cycle re-score: scorecard §19 maturity 3→4 CONFIRMED on the StateManagementConventionTests CI.slnf gate; implementation held at 9 after a first-pass 9→8 proposal was adversarially rejected as unsupported). The 2026-07-02 reopening (no §19 fitness gate) is answered by the wave-2 gate below
(Medium) The— RESOLVED: all six Conference create forms now pass the framework live-accessorUnsavedChangesGuardparam-lag... spurious "unsaved changes" prompt after a successful createIsDirtyAccessor="() => _isDirty". The MMCA.CommonUnsavedChangesGuardlive-accessor (shipped v1.51.0) reads dirty state at navigation time, eliminating the one-render parameter lag — noStateHasChanged()-before-NavigateTodance needed.
Fix
- Adopt the framework live-accessor guard (MMCA.Common #19, shipped v1.51.0) on all six create forms; supersedes the
StateHasChanged()-before-NavigateToworkaround. - (maturity 3→4 lever) DONE 2026-07-11 (remediation wave 2): the §19 state-management fitness gate now runs in the CI.slnf arch gate:
StateManagementConventionTests(sealed subclass of the shared v1.115.0StateManagementConventionTestsBase) reflects over the three module UI assemblies (registered asLayer.UiinAdcArchitectureMap) 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 Conference.UI failed the gate with the exact offender name, green after removal). Landed in the same wave as the #18 gate, as planned. Candidacy CONFIRMED on the 2026-07-15 twentieth-cycle re-score: scorecard §19 maturity 3→4 (impl held 9).
[ ] #21 · Accessibility · 3 → 4 (weight 3, priority (4-3)×3=3) · scorecard §21 maturity 3 / impl 8 (2026-07-02 fifteenth cycle, re-confirmed 2026-07-03): the axe layer is an enforced deploy gate (chromium e2e-gate in deploy.needs) and the broadened 17-page scans went green on validation run 28604877733 (impl 7→8). Maturity stops at 3 because the rubric pairs automated CI checks with a recorded manual screen-reader pass, which ACCESSIBILITY-SCREENREADER-PASS.md still awaits: that recorded pass is the cheapest maturity 3→4 lever (needs a human + NVDA/VoiceOver)
- (Low) a11y is implemented (aria-labels, alt text, real links/buttons) but never auto-verified — no axe/Lighthouse in CI, no
AccessibilityTests, no stated WCAG target.
Fix
- Add automated a11y checks and a stated WCAG 2.1 AA target → DONE:
Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/AccessibilityTests.csruns axe-core WCAG 2.1 AA scans (broadened to 17 pages on 2026-06-30); the target is stated inCLAUDE.mdandACCESSIBILITY-SCREENREADER-PASS.md. (Deploy-gated since 2026-07-02: the scans ride the chromiume2e-gatejob indeploy.needs.) - (impl 7→8 lever) Stand up a backend-less in-process axe merge-gate, mirroring MMCA.Common's gallery-host pattern → SUPERSEDED (2026-07-02): the full axe suite became the deploy-gating
e2e-gate, which delivered the impl 8 and the enforcement this scoped backend-less host targeted, so the separate host is no longer needed for the score. (Still available as an architecture option if the full-suite gate ever has to be demoted.) - (maturity 3→4, cheapest open win) Record a dated manual screen-reader pass in
ACCESSIBILITY-SCREENREADER-PASS.md(needs a human + NVDA/VoiceOver against the running Aspire app; cannot be done headless, so it stays pending a human run). - (NEW 2026-07-12, latent contrast in state-gated Warning-outlined surfaces, effort S.) Store's gated axe scan caught that an OUTLINED
MudAlert Severity="Severity.Warning"renders its text in the Warning amber (#F57F17, ~2.6:1 on white, AA fail) the moment a state-gated banner actually rendered during a scan (Store run 29191273727; fixed there by switching toSeverity.Infooutlined). ADC carries the same latent pattern in at leastSpeakerDashboard.razor:37andSessionFeedback.razor:29(plus amberVariant.OutlinedColor.Warningbuttons onEventDetail.razor:141and the bookmarked-state toggle onPublicSessionDetail.razor:136); the 17-page axe gate is green only because those states are not exercised by the scans. FIXED 2026-07-16 (all six sites, two more than recorded): the four outlined Warning alerts switched toSeverity.Infooutlined (Store parity; the sweep also caughtPresenterView.razor:21andSessionLive.razor:21), and the two outlined amber buttons moved to the AA-passing Secondary teal (EventDetailUnpublish, and the bookmarked state ofPublicSessionDetail's toggle, whose filled-star icon keeps the state signal). Repo-wide grep for outlined Warning surfaces is now zero. CI.slnf 2073 green. - (shared with #28) Promote the full axe + E2E suite to a merge gate → DONE (2026-07-02): promoted as the chromium
e2e-gateindeploy.needsafter validation run 28604877733 (the first fully green three-browser matrix); firefox/webkit stay advisory on the nightly (#22).
[x] #18 · UI Architecture & Component Design · 3 → 4 (weight 3) · RESOLVED 2026-07-15 (twentieth-cycle re-score: scorecard §18 maturity 3→4 CONFIRMED on the UIArchitectureConventionTests CI.slnf gate; implementation holds 9). The 2026-07-02 reopening (no §18 UI-architecture fitness gate; the route-auth tests were a §25 gate wrongly credited here) is answered by the wave-2 gate below
- (Low) No bUnit tests, no UI fitness function; one 425-line code-behind. (Original 2026-06-08 finding: bUnit tests have since shipped, but a UI-architecture fitness gate never did, so the 2026-07-02 re-score withdrew the maturity-4 that had credited the route-auth tests as a §18 gate.)
Fix
- Add component tests (shared with #28) + a UI convention test: bUnit projects shipped. The "UI convention test" credited here was
ManagementRouteAuthorizationTests, which is a route-authorization gate (§25), not a §18 UI-architecture gate, so it did not on its own earn maturity 4 (corrected on the 2026-07-02 re-score). - (maturity 3→4 lever) DONE 2026-07-11 (remediation wave 2): the §18 UI-architecture fitness gate now runs in the CI.slnf arch gate:
UIArchitectureConventionTests(sealed subclass of the shared v1.115.0UIArchitectureConventionTestsBase) caps every*.razor.csunder Source/ at 400 lines and inline@codeblocks at 120 lines. Verified non-vacuous via a seeded 402-line file. Subsumed TD-13 (below) and additionally forced conforming splits ofSessionLive.razor.cs648→357 (three extracted panels) andPublicSessionList.razor.cs499→371 (filter bar + view components), which had grown past the cap since TD-13 was recorded. Repo-wide max code-behind is now 387 lines. Candidacy CONFIRMED on the 2026-07-15 twentieth-cycle re-score: scorecard §18 maturity 3→4. - TD-13 DONE 2026-07-11 (remediation wave 2, subsumed by the §18 gate above): both named code-behinds split via presentational sub-component extraction, markup moved verbatim (rendered DOM unchanged for the E2E selectors):
SessionSelectionDashboard.razor.cs507→367 (extractedSessionSelectionSpeakerOverlap,SessionSelectionAiScores, and the pure-rulesSessionSelectionDisplayhelper) andSpeakerDetail.razor.cs429→368 (extractedSpeakerCategoryItemsPanel). Conference UI bUnit suite green (105/105) after each split. - TD-16 (recorded 2026-07-21, the §18 impl 8→9 lever, effort S): the largest code-behind now sits flush at the convention ceiling with zero headroom.
Source/Modules/Engagement/MMCA.ADC.Engagement.UI/Pages/HappeningNow/HappeningNow.razor.csis exactly 400 lines against theMaxCodeBehindLines => 400cap (MMCA.Common.Testing.Architecture/Bases/UIArchitectureConventionTestsBase.cs:22), so the next method added to it fails the gate rather than being caught in review. Six more files sit in the 360-379 band (SessionSelectionDashboard 379, SessionDetail 376, PublicSessionList 367, SpeakerDetail 365, EventDetail 365, SessionLive 362). Blocker: none, this is scheduled work. Resolution path: presentational sub-component extraction per the TD-13 pattern above, markup moved verbatim so the rendered DOM and the E2E selectors are unchanged. Effort: S. This is what took scorecard §18 implementation from 9 to 8 in the twenty-second cycle; maturity holds 4 on the gate.
[x] #8 · Data Architecture · 3 → 4 · RESOLVED 2026-06-29 (scorecard §8 maturity 4 / impl 9); TD-03 concurrency round-trip CLOSED 2026-07-06 (implemented + deploy-gated, Conference-only, so impl holds 9)
(Low) The orphaned integration suite means soft-delete/concurrency/outbox/migration behaviors have no ADC-repo regression coverage.— per-service integration tests restored (#14) exercise CRUD/auth/ownership against real per-service SQL DBs; migration drift + soft-delete fidelity now guarded.
Fix
- Restore per-service integration tests (done via #14).
- Migration model-drift gate —
build-and-testnow runsdotnet ef migrations has-pending-model-changesfor all four modules (Identity/Conference/Engagement/Notification) on the Release build (--no-build, no DB needed). Fails the build — and so the deploy — if an entity changed without a matching migration. Verified locally: all four currently report "No changes" (drift-free). - Soft-delete fidelity test —
SoftDeleteFidelityTests(Conference integration tier) deletes an Event via the API, asserts it's hidden by the EF global query filter (404), and reads[Conference].[Event]directly to prove the row survives withIsDeleted = 1(soft- not hard-delete). The fixture now exposes itsConnectionStringfor raw-table assertions. - Outbox-dispatch fidelity —
OutboxFidelityTests(Identity tier) registers a user and asserts aUserRegisteredrow landed in[dbo].[OutboxMessages](confirmedInProcessEventBus.PublishAsyncpersists the row transactionally, then marks it processed — the row is retained). The Identity fixture now exposesConnectionString. (TD-04 — done 2026-06-13, effort S.) - TD-03 RESOLVED (2026-07-06): optimistic-concurrency API round-trip now implemented and deploy-gated. The Conference
EventDTOcarries theRowVersiontoken viaIConcurrencyAware(Conference.Shared/Events/EventDTO.cs:16),UpdateEventHandler.cs:34stamps the client's last-seen token withSetOriginalRowVersion(a stale token then raisesDbUpdateConcurrencyException, whichDbUpdateExceptionHandlermaps to 409), andOrganizerConcurrencyTests.cs:26(Update_WithStaleRowVersion_ReturnsConflict) asserts the 409 inside the deploy-gatingMMCA.ADC.Integration.slnf(theintegration-testsjob is indeploy.needs). Round-trip is Conference-only (Identity/Engagement expose no token-carrying update endpoint), so scorecard §8 holds impl 9 (not 10). The Common seam (SetOriginalRowVersionon the repository) shipped and ADC adopted it on the five Conference update handlers.
[x] #1 · SOLID Principles — 3 → 4 · ctor-dependency-count fitness threshold landed (scorecard §1 stays M4/I9 — protect)
(Low)GUARDED (v1.86.0 sweep, 2026-06-27): kept as the cohesive auth facade, but a ctor-dependency-count fitness function now holds the line:AuthenticationServicehas 7 constructor dependencies (down from 9 — validators bundled intoAuthenticationValidators) and injects a command handler directly.Source/Modules/Identity/.../Users/AuthenticationService.cs:21-28.AuthenticationServicesits at the 7 high-water mark and an 8th dependency would fail the build.
Fix
- Acceptable as a cohesive auth facade; the ctor-dependency-count fitness threshold is now landed:
Tests/Architecture/MMCA.ADC.Architecture.Tests/ConstructorDependencyCountTests.cscaps constructor dependencies at ≤7, withAuthenticationServiceat the 7 high-water mark.
🟢 Priority 2 — score 3, weight 2 (polish / hardening)
[x] #9 · API & Contract Design · Resolved 2026-06-12
(Medium) No OpenAPI served by any running service, yet CLAUDE.md still advertises 4 doc UIs (/swagger,/nswag-swagger,/api-docs,/scalar/v1).- Serve OpenAPI per service → all four service hosts now register
AddOpenApi()+ map/openapi/v1.json(built-inMicrosoft.AspNetCore.OpenApi, package wired via the.Serviceconvention inDirectory.Build.props). Mapped outside Production only — these are internal services reached through the Gateway, which does not route the endpoint. The ApiExplorer group ('v'VVV→v1) matches the default document name, so the controller surface populates. - Fixed the stale CLAUDE.md OpenAPI bullet (the four advertised UIs were a carry-over from the deleted WebAPI host; corrected to the
/openapi/v1.jsondocument). - Contract test (
OpenApiContractTestsinMMCA.ADC.Conference.IntegrationTests) boots the real host and asserts the document is served, is well-formed OpenAPI 3.x describing ≥ 10 routes, and still exposes the core public resources (/Events,/Sessions,/Speakers) — so an accidental route removal fails CI. Runs in the integration-tests tier, which gates deploy. - Versioning proven beyond v1.0 (2026-06-19).
ServiceInfoController(Conference) serves/ServiceInfoat v1.0 (deprecated) and v2.0, selected by theapi-versionheader — exercisingMapToApiVersionrouting + deprecation reporting (ReportApiVersions).ApiVersioningTests(integration tier) asserts each version returns its own shape and that theapi-supported-versions/api-deprecated-versionsheaders are emitted, so the versioning machinery is exercised, not merely configured for a single version. - Deferred: interactive UI (Scalar/Swagger) — the three REST services are h2c-only on cleartext, so a browser can't reach a service-hosted UI directly; a Gateway-routed UI is a small follow-up if wanted.
[x] #34 · Architecture Governance & Documentation · Resolved 2026-06-13
(Medium) CLAUDE.md says "the .NET code is not yet wired to Service Bus" while Bicep wires— ✅ fixed 2026-06-08 (CLAUDE.md broker note corrected; provider switch + Standard-tier/Manage gotchas documented).MessageBus__Provider=AzureServiceBusin prodRemaining: no ADR for the monolith→services extraction; fitness tests lag the new topology.- Correct the broker note in CLAUDE.md (it's wired in prod).
- Write the extraction ADR →
ADRs/008-service-extraction-topology.md(monolith → 4 services + Gateway; ties together the facet ADRs 003/004/006/007). README index updated. - Update fitness tests for the service topology → new
MicroserviceExtractionTests(12 tests) enforce transport-at-the-edge: no gRPC / MassTransit / Protobuf dependency in any Domain, Application, or Shared assembly — making the guard ADR-007 claimed (but that never existed) real. Full architecture suite green (110 tests, run locally — no SQL needed).
[x] #17 · DevOps & Deployment · 2 → 4 · RESOLVED 2026-06-29 (scorecard §17 maturity 4 / impl 9; managed-identity SQL auth active in prod); SQL private endpoints deferred-by-design
- (Medium) Runtime uses shared/admin keys (ACR admin password, SQL keys), not managed identity;
no rollback or post-deploy smoke gate— post-deploy smoke gate + auto-rollback added (deploy.ymlPhase 5: Gateway/health+ JWKS + UI probes →az containerapp revision copyrollback on failure; documented ininfra/DISASTER-RECOVERY.md). - Switch runtime auth to managed identity — DONE: ACR pull via the shared UAMI (admin password gone) and all runtime secrets moved to RBAC Key Vault (read via managed identity). Add a rollback path + post-deploy smoke gate → DONE. (ACR admin user disabled — no admin credential exists.)
- Switch app→DB SQL auth to managed identity (pairs with #11) — DONE (2026-06-28):
useManagedIdentitySql=trueactivated in prod via the stagedinfra/SQL-MANAGED-IDENTITY.mdsequence (deploy.yml repo-var passthrough → Entra admin → per-DBCREATE USER ... FROM EXTERNAL PROVIDER+db_owner→ flag flip). All four services run passwordless onAuthentication=Active Directory Managed Identitywith asdb_ownerin every per-service DB (verified Healthy on the new revisions). The shared SQL password is gone from all connection strings; the SQL admin login is a dormant fallback. Lifts §17 impl 8→9. - (Residual, deferred-by-design) Move the SQL data plane onto private endpoints (disable public network access, drop the 0.0.0.0 firewall). The VNet + private-endpoint epic (recreates the Container Apps environment), documented-accepted in
infra/SQL-MANAGED-IDENTITY.md. This is the only remaining §11 impl 9→10 lever now that the credential flag is closed.
[x] #24 · Forms, Validation & UX Safety · 3 → 4 (weight 2) · RESOLVED 2026-06-30 for MATURITY (scorecard §24 maturity 4: FormsConventionTests in the CI.slnf arch gate enforces the unsaved-changes guard + dirty tracking + validated MudForm across the six create forms). Implementation recalibrated 9→7 on the 2026-07-03 re-score (error presentation was overstated), then recovered 7→8 on the 2026-07-15 twentieth-cycle re-score (TD-14 shipped, see below). The category header stays closed: maturity holds 4 on the gate
(Medium) Silent data loss on all six inline-edit paths (Detail pages have no unsaved-changes guard)— RESOLVED:UnsavedChangesGuard(withIsDirtyAccessor) +MarkDirty/_isDirtydirty-tracking added to all six Detail edit forms (Event/Speaker/Room/Session/Question/ConferenceCategory);_isDirtyresets on edit-enter, cancel, and successful save. Build clean, 1244 ADC CI tests green.(Medium) Profile change-password form lacked client-side match/Required validation and used a generic snackbar rather than a per-form error summary— RESOLVED (v1.86.0 sweep, 2026-06-27):Identity.UI/Pages/Profile/Profile.razor:29,33,37addsRequired+RequiredErrorto all three fields,:38wires client-side match (ValidateConfirmPassword) alongsideValidateNewPassword, and:41-52renders a per-formMudAlerterror summary;Profile.razor.cs:40-43(match),:84-88(ValidateAsync gate before submit),:56/:90(Disabled while saving). Closes the last §24 scorecard deduction (impl 8→9).- Apply
UnsavedChangesGuard+ dirty tracking to the Detail/inline-edit pages (pairs with #19). - Add client-side match/Required validation + a per-form error summary to change-password — done on the v1.86.0 sweep (evidence above).
- (maturity-4 lever) Add an automated forms / unsaved-changes / validation convention fitness test → DONE 2026-06-30:
Tests/Architecture/MMCA.ADC.Architecture.Tests/FormsConventionTests.csscans the six Conference*Create.razorforms and fails the build if any drops itsUnsavedChangesGuard(with a liveIsDirtyAccessor),_isDirtytracking, validated<MudForm, orRequired/RequiredErrormarkers (runs in the CI.slnf arch gate, verified green). Lifted scorecard §24 maturity 3→4. - TD-14 CLOSED 2026-07-11 (remediation wave 6), the §24 impl 7→8/9 lever: both remaining pieces landed. (a) All six Conference create forms now render the same per-form
MudAlerterror summary the Profile form pioneered (localizedValidation.CorrectFollowingheading + the_form.Errorslist, en+es key pairs added to all six form resx pairs; the snackbar kept as the secondary channel). (b)FormsConventionTestsnow covers the Profile form via a dedicated fact (error summary +ValidateNewPassword/ValidateConfirmPasswordwiring + the three Required password fields) AND hardens the create-form gate by appending the error-summary markers toRequiredMarkers, so the new presentation cannot silently regress. CI.slnf 2066 tests green. Candidacy CONFIRMED on the 2026-07-15 twentieth-cycle re-score: scorecard §24 impl 7→8 (held at 8, not 9: the summary conventions are string-marker enforced; a render-level bUnit assertion of the summary's error items is the 8→9 lever). (Historical: the raw{ex.Message}snackbars were eliminated on the 2026-07-03 i18n sweep.) 8→9 lever SHIPPED 2026-07-16:EventCreateTests.SubmittingBlankForm_RendersThePerFormErrorSummaryWithItemsrenders the form, fails validation, and asserts the summaryMudAlertactually renders with its localized heading and per-error list items (render-level proof beside the string-marker gate). Runs in the CI.slnf bUnit tier. §24 impl 8→9 candidacy recorded for the next re-score.
[x] #13 · Observability & Operability · 3 → 4 (weight 2) · RESOLVED 2026-07-17 (twenty-first-cycle re-score: scorecard §13 maturity 3→4 CONFIRMED on the ObservabilityConventionTests alert-runbook pairing gate in the CI.slnf arch gate, MMCA.ADC.CI.slnf:56 + deploy.yml:57,417; implementation holds 9). The 2026-07-15 REOPENING is closed: the exact lever it named (a CI gate over the sloAlertSpecs/OPERATIONS.md pairing) shipped 2026-07-16
(Medium) No alerting / SLOs / dashboards / runbooks (App Insights is wired but passive).- Add alerts + action groups, dashboards, and basic runbooks (overlaps #29). → alerts + action group done (3 App-Insights SLO metric alerts in
main.bicep); recovery runbook done (infra/DISASTER-RECOVERY.md); dashboard/workbook done (sloWorkbookAzure Monitor workbook inmain.bicep←workbooks/adc-slo-workbook.json, mirroring the SLO alerts per service). - Day-2 operational runbooks DONE (2026-07-11):
infra/OPERATIONS.mdmaps each provisioned alert (failed-requests,server-response-time,dependency-failures) to concrete triage steps (workbook pane, App Insights drill path, per-service container logs, the auth/gRPC/outbox failure classes) plus the fast-reference recovery moves (revision rollback, PITR restore, the three freshness gates, surge revert) and a pair-with-sloAlertSpecsgovernance note. Adjudicated 2026-07-15: the runbook/workbook substance lifted scorecard §13 impl 8→9, but the maturity 3→4 candidacy was rejected (review-enforced, not CI-gated); the category stays open at M3/I9. - Maturity gate SHIPPED (2026-07-16, the reopened lever):
Tests/Architecture/MMCA.ADC.Architecture.Tests/ObservabilityConventionTests.csmachine-enforces the alert-to-runbook pairing in the CI.slnf arch gate (runs on every PR and gates deploy): everysloAlertSpecsentry ininfra/main.bicepmust keep a### ...-alert-<key>section ininfra/OPERATIONS.mdwhose heading carries the alert's current(sev N), orphan runbook sections fail, and a minimum-spec floor (3) keeps the parse non-vacuous. Both files are embedded resources of the test assembly, so the gate sees exactly what ships. Verified red on a seeded severity drift (sev 2 to 4 flagged with the exact heading) and green on the real files. TheOPERATIONS.md"change a threshold and this file together" governance note is now enforced, not advisory. Maturity 3→4 candidacy recorded for the next re-score. Mirror planned for Store #13 (same gate shape). Adjudicated 2026-07-17 (twenty-first cycle): ACCEPTED; scorecard §13 M4/I9, category closed (protect).
[~] #22 · Responsive & Cross-Browser · 3 → 4 (weight 2, priority (4-3)×2=2) · REOPENED 2026-07-21 (twenty-second-cycle re-score: scorecard §22 maturity 4→3, implementation holds 8). The 2026-07-16 lever that closed this item was undone on 2026-07-18 by the Actions-minute reduction: the deploy e2e-gate now invokes browsers: '["chromium"]' (deploy.yml:488, rationale in the job comment at :478-480, job still in deploy.needs at :783), so firefox and webkit run only on the weeknight nightly matrix (e2e.yml:39, skipped when the branch has not advanced, :85-95), where they are continue-on-error (e2e.yml:119). Cross-engine verification is nightly-advisory again, which is maturity 3. This is a deliberate cost trade-off, recorded in Deliberate / accepted, not a regression in the responsive work
(Medium) E2E is Chromium-only; no documented browser/device matrix.- Define a support matrix; add a non-Chromium E2E pass (or document the limitation).
- (maturity 3→4 lever, REOPENED 2026-07-21): restore enforced cross-engine coverage.
deploy.yml'se2e-gatewas cut tobrowsers: '["chromium"]'on 2026-07-18 for Actions-minute savings (deploy.yml:478-480,488), so firefox/webkit now run only on the nightlyschedule, wheree2e.yml:119keeps themcontinue-on-error. Options: (a) re-add the two legs to the deploy gate (3x runner minutes, the 2026-07-16 shape), (b) add across-browser-freshnessjob todeploy.needsmirroring the existing dr / load / cross-service freshness gates (deploy.yml:496,553,610) so a stale or red nightly matrix blocks the deploy at near-zero minute cost, or (c) record the chromium-only gate as permanent and accept §22 at maturity 3. Option (b) is the cheapest reconciliation of the cost goal with the gate. Prior closure (2026-07-16/17) is preserved in history below. - Closed 2026-07-16/17, undone 2026-07-18: the three-browser gate did ship and was adjudicated ACCEPTED in the twenty-first cycle (8 consecutive fully-green nightly matrices, 2026-07-09 through 2026-07-16, firefox + webkit job conclusions verified per run). The CI-minute program then reverted it as a cost measure.
- Status 2026-06-20: firefox + webkit do run in the nightly matrix (advisory
continue-on-error) but are still red alongside chromium, so the non-Chromium pass is not green yet (see the #28 nightly-watch note). - Status 2026-07-03 (re-score, scorecard §22 M3/I8): validation run 28604877733 (2026-07-02) was fully green across all three engines, and the support matrix is documented in
CLAUDE.md. Only the chromium leg gates deploy (e2e-gate); firefox/webkit remain advisory on the nightly, so cross-browser verification is still not an enforced gate (the maturity 3→4 lever: gate the non-chromium legs after a reliably green soak).
[x] #25 · Navigation & Information Architecture — RESOLVED (Wave 2)
Admin pages are hidden-but-routable (— RESOLVED: the 18 master-data management routes now carry[Authorize]only, no role attribute).@attribute [Microsoft.AspNetCore.Authorization.Authorize(Roles = "Organizer")]— Event/Session/Room/Question/ConferenceCategory (list+create+detail), Speaker (list+create), and IdentityUserList(/users). These already had the server-side gate ([Authorize(Policy = AuthorizationPolicies.RequireOrganizer)]on the controllers, reads[AllowAnonymous]); the route attributes were the missing UI/IA layer, so this is defense-in-depth + no more attendee-visible dead-end pages. Build clean (0/0).- Deliberately left bare
[Authorize]:SpeakerDetail(/speakers/{id}) — PUT/speakers/{id}is[Authorize](ownership-checked) so a speaker self-edits their own profile there;SpeakerDashboard, Engagement feedback, and IdentityProfile/UserClaimsare self/attendee-facing.
- Deliberately left bare
- Add role-based authorization (
[Authorize(Roles)]) to admin routes. - Residual: DONE —
ManagementRouteAuthorizationTests(reflection fitness test inConference.UI.Tests) asserts every admin-namespace page keeps[Authorize(Roles="Organizer")]so a route can't silently drop to bare[Authorize]. IdentityUserList(/users) is covered by the parallelIdentityRouteAuthorizationTestsin the newIdentity.UI.Testsproject.
[x] #6 · CQRS & Event-Driven · 2 → 4 · RESOLVED 2026-06-29, scorecard §6 maturity 4 / impl 9 as of the 2026-07-02 re-score: impl corrected 10→9 because the idempotent inbox covers only 2 of 4 consumer services (Conference appsettings.json:32, Identity :29; Engagement/Notification carry none), so real levers remain. Broker round-trip TD-02 is now the impl 9→10 lever; the category stays maturity 4 (protect)
No event-schema versioning; ID-dependent events published post-commit (intentional — the post-commit publish is how events carry DB-generated identities);broker round-trip tests excluded(still deferred — needs a RabbitMQ container).- Event-contract guard —
IntegrationEventContractTests(architecture tier) reflects over everyIIntegrationEventin the module Shared assemblies and snapshots its declared shape (property name + type) against a frozen baseline. A renamed/removed/retyped property — or a new event added without snapshotting — fails the build, forcing a conscious version/rollout decision. The async counterpart to #9's REST contract test; runs in CI build-and-test (no broker/SQL needed). Verified locally (111 architecture tests green). - Idempotent inbox enabled on the consumers (2026-06-19).
MessageBus:EnableInbox=trueinIdentity.Service+Conference.Serviceappsettings (the two services that consume integration events; each already ships theInboxMessagestable via itsAddInboxMessagesmigration). Dedup is now verified in MMCA.Common byEfInboxStoreTests(real SQLite + the production unique index → a redelivered message id records exactly once). Converts consumer idempotency from convention to infrastructure. - *§6 Implementation 9→10 lever, TD-02 CLOSED 2026-07-11 (remediation wave 6):* both remaining pieces landed. (1) The broker round-trip now gates the deploy via recency: a
cross-service-freshnessjob indeploy.yml'sneedsfails a deploy when the latest successful nightlycross-service-tests.ymlrun is older than 3 days (the dr/load-freshness pattern; the Testcontainers workflow itself still never runs inside the deploy chain, which the Docker constraint forbids and its header comment now documents). (2)MessageBus:EnableInbox=trueon all four consumer services: Engagement and Notification appsettings joined Conference + Identity (theirInboxMessagestables shipped with the 2026-06-09AddInboxMessagesmigrations, applied in prod by the sole-migrator startup path). §6 Implementation 9→10 candidacy recorded for the next re-score. (Historical context: the tier landed 2026-07-06 as 9 Testcontainers RabbitMQ+SQL dual-host tests.)
[~] #12 · Performance & Scalability · 3 → 4 (weight 2, priority (4-3)×2=2) · OPEN at scorecard §12 M3/I8 (twentieth-cycle adjudication, re-confirmed 2026-07-17; a stale nineteenth-cycle "RESOLVED 2026-07-12 M4/I8" header accidentally committed via PR #15 is corrected here). The k6 proof's recency gates the deploy (load-freshness, deploy.yml:553, in deploy.needs at :783) and the WebVitals budgets are enforced inside the e2e-gate (§23's credit), but the k6 tier itself executes monthly/dispatch out of band and the Notification app stays pinned maxReplicas: 1 (infra/main.bicep:1424), so maturity holds 3. Re-confirmed 2026-07-21 (twenty-second cycle), with one nuance newly verified: all three recency gates accept a skip_freshness_gates dispatch input with a required justification (deploy.yml:520,577,636), so the k6 recency proof is bypassable-with-justification rather than unconditional (see Deliberate / accepted)
No load testing→ DONE: the k6conference-read-load.jsload test runs in CI sized to the measured ~67 peak. The SignalR multi-replica/backplane risk is resolved into a documented single-replica acceptance (Notification pinnedmaxReplicas: 1,main.bicep:1007-1012).- (impl-8 lever) Add client-side Core Web Vitals measurement to the E2E suite → DONE 2026-06-30: a
WebVitalsTestsPlaywright tier (Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/WebVitalsTests.cs+Infrastructure/WebVitalsCollector.cs) injectsPerformanceObservers to capture LCP/CLS/FCP/TTFB on/,/conference/events,/login(plus a single-interaction INP sample on the data-grid page), asserts lenient budgets, and emits a datedweb-vitals-*.jsonartifact (wired intoe2e.ymlviaWEB_VITALS_OUTPUT_DIR). Both the backend k6 and the client-side vitals are now measured, closing the residual gap and lifting scorecard §12 Implementation 7→8. Test/CI-only (noMMCA.Commonrelease); builds clean. (Maturity held at 3: the vitals run nightly/dispatch like k6, not as a merge gate.) - Deferred (optional), provisioning half DONE: prod Redis is provisioned (
infra/main.bicep:740Microsoft.Cache/redisEnterprise@2024-09-01-preview, database at:753,redis-connection-stringsecret injected at:771,849-850), so the shared cache / SignalR backplane substrate exists. The fan-out itself stays unexercised: Notification is still pinnedmaxReplicas: 1(infra/main.bicep:1424, deliberate right-sizing comment at:1422), so a verified two-replica hub fan-out remains the §12 impl 8→9 lever and this category stays open. - (maturity 3→4 lever) DONE 2026-07-11 (remediation wave 3): the capacity checks are now enforced deploy preconditions: (a) a
load-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 624ms, 4-30x headroom), asserted inside the deploy-gating chromiume2e-gate(e2e.yml runs the whole E2E project). Adjudicated 2026-07-15 (twentieth-cycle re-score): the §23 half was ACCEPTED (scorecard §23 maturity 3→4 on the enforced CWV budgets) but the §12 half was REJECTED: §12 holds M3/I8 (the k6 tier is freshness-gated but still nightly/manual in execution, and the Notification app stays pinnedmaxReplicas: 1,infra/main.bicep:1113), so this category stays open at maturity 3.
[x] #5 · Vertical Slice Architecture · 3 → 4 (weight 2) · RESOLVED 2026-06-30 (scorecard §5 maturity 4 / impl 8): the slice-cohesion fitness function is now a confirmed CI merge gate (Optimized process maturity); the deliberate layered-by-project hybrid remains the accepted impl-8 cap
- The deliberate layered-by-project hybrid is accepted; the line is now held by a fitness test that runs in the CI arch gate.
- Subclassed the framework's shared slice-cohesion fitness function (
SliceCohesionTestsBase, MMCA.Common.Testing.Architecture):Tests/Architecture/MMCA.ADC.Architecture.Tests/SliceCohesionTests.cs:8, verified passing across all three modules. (Shipped via the lockstep sweep to MMCA.Common.* v1.85.0.) The impl 7→8 lift closed on that sweep. - (maturity-4 confirmation, v1.93.0 re-score) The slice-cohesion test runs in
MMCA.ADC.CI.slnf:54, so it gates every push/PR (the rubric's M4 "enforced automatically by tests/CI");ArchitectureRules.Slices.cs:31fails the build when a handler/validator is stranded from its same-assembly contract. Scorecard §5 reaches maturity 4, impl held at 8 by the conscious layered-by-project hybrid.
[x] #16 · Maintainability & Evolvability · 3 → 4 (weight 2) · RESOLVED 2026-06-30 (scorecard §16 maturity 4 / impl 8): FrameworkVersionConsistencyTests (CI.slnf arch gate) now fails the build if any MMCA.Common.* package diverges from the single lockstep version, so ADR-016 consistency is enforced not merely followed
lingering non-building test projects— RESOLVED:Tests/WebAPIrevived asMMCA.Common.APImiddleware unit tests; the orphaned combinedMMCA.ADC.IntegrationTestswas superseded by the per-service integration projects (#14) and, once its single-service tests were re-homed and its headline cross-service flows restored (#14 Phase 4), the project folder was physically deleted — so no non-building legacy test csproj remains in the tree (the orphan-test cleanup shipped, but the 2026-06-29 re-score holds §16 at maturity 3: process is Consistent, not yet fully Optimized).- Tech-debt tracking — every deferred sub-item carries a
TD-NNID with its blocker + resolution path + effort, and the recorded-not-scheduled choices have a Deliberate / accepted section. (Originally a separateTECHDEBT.md(TD-01…TD-10); folded into this backlog 2026-06-26 as the single per-repo ledger, matching MMCA.Common and MMCA.Store.) - Doc drift (#34) — fixed (broker note corrected; extraction ADR + fitness tests landed under #34).
- (maturity-4 lever) Reach Optimized §16 process maturity → DONE 2026-06-30: added
Tests/Architecture/MMCA.ADC.Architecture.Tests/FrameworkVersionConsistencyTests.cs, a fitness check that readsDirectory.Packages.propsand fails the build if the thirteenMMCA.Common.*packages are not all pinned to one version (catching a partial sweep), so the lockstep-version consistency is enforced not merely followed. The remaining residual is cosmetic (the frozen combinedMMCA.ADC.Migrations.SqlServerarchive csproj; the workspaceArchitecturalAnalysis.mdoutside any repo), acceptable.
[x] #20 · Design System & UI Consistency · 2 → 4 · RESOLVED 2026-06-29 (scorecard §20 maturity 4 / impl 9); residual Secondary-token / !important drift is Common-side (see Deliberate / accepted)
Landing page hardcodes brand hex and is duplicated across two hosts; no automated consistency check.RESOLVED (v1.86.0 sweep, 2026-06-27): the ADC landing page is now brand-token-clean:ADCHome.razor.css:215,252,277in both UI hosts usevar(--mmca-primary), guarded byTests/Architecture/MMCA.ADC.Architecture.Tests/BrandColorTokenTests.cs:26-37(a consistency fitness function). Lifted scorecard §20 impl 8→9.- Centralize the brand token; dedupe the landing page; add a consistency check. → done (evidence above).
- Residual (Common-side, OPEN):
BrandColorTokenTestsguards Primary only (Secondary has no drift test), and a few!importantoverrides + Store-specific cart CSS live in Common's sharedapp.css. These are MMCA.Common changes, not ADC-local.
[x] #23 · Front-End Performance · RESOLVED 2026-07-15 for MATURITY (twentieth-cycle re-score: scorecard §23 maturity 3→4 CONFIRMED on the enforced CWV budgets inside the deploy-gating chromium e2e-gate; implementation holds 8, the code-split/image polish below stays open)
No Core Web Vitals/RUM; WASM not code-split; images unoptimized.- Add CWV tracking → DONE + GATED (2026-07-11, remediation wave 3): CWV was measured per E2E run since 2026-06-30 (
WebVitalsTests); the budgets are now the enforced Core Web Vitals "good" band asserted inside the deploy-gating chromiume2e-gate(see the #12 wave-3 note above), closing the "advisory by design" hold from the nineteenth-cycle re-score. Candidacy CONFIRMED on the 2026-07-15 twentieth-cycle re-score: scorecard §23 maturity 3→4. - (Impl polish, open) code-split WASM; optimize images.
[x] #31 · Cost Efficiency / FinOps · 3 → 4 (weight 2) · RESOLVED 2026-06-30 (scorecard §31 maturity 4 / impl 8): cost-guard.yml is now a workflow_call reusable workflow invoked as a cost-guard job in deploy.needs, so a deploy is blocked while a surge is un-reverted (committed, activates on the next push)
No budgets/alerts, no cost tags, no scheduled scale revert.(Baseline was already cost-minimal — all SQL Basic, all apps min1/max2; the conference surge had been reverted in Bicep. The gap was the absence of guards/attribution.)- Budget + cost alerts —
Microsoft.Consumption/budgetsinmain.bicep: a monthly RG budget (monthlyBudgetAmount, default $200) notifying the existing action group +alertEmailAddressat 80% actual and 100% forecasted spend. The automatic guard against an un-reverted surge silently billing for weeks. - Resource cost tags —
commonTags(application/environment/component/managedBy/costCenter) stamped on every billable resource acrossmain.bicep(App Insights, SQL server + all 5 DBs, Service Bus, Container App env, all 6 Container Apps) andfoundation.bicep(ACR, Log Analytics) for Cost-Analysis attribution. - Scheduled surge-drift guard —
.github/workflows/cost-guard.yml(weekly cron + manual) is a read-only check that everyadc-*Container App is ≤maxReplicas 2and every SQL DB is Basic; on drift it fails the run (GitHub-notifies) and prints how to reset. Read-only by design — auto-mutating prod on a schedule would clobber an intentional surge and risks revision churn; the budget covers the $ side, this covers the config side. - (maturity-4 lever) Lift FinOps process maturity beyond the scheduled read-only
cost-guard.yml→ DONE 2026-06-30: added aworkflow_calltrigger tocost-guard.ymland acost-guardjob (uses: ./.github/workflows/cost-guard.yml,secrets: inherit) todeploy.yml'sdeploy.needs, so the read-only surge-drift check now gates the deploy (a deploy is blocked while a conference-day scale-up is un-reverted) rather than only flagging weekly. Lifted scorecard §31 maturity 3→4. Committed; activates on the next push. - TD-15 (deferred, recorded 2026-07-19) · Topology collapse: one host + one DB, no bus/Redis/gateway (effort L). The framework already supports collapsing the distributed topology back into a modular monolith with NO application-code rewrite:
AddBrokerMessagingfalls back to the in-process bus when no broker is configured,DataSourceResolvercollapses the per-module logical sources onto one physical database (single context, FK constraints restored), andModuleLoaderboots all four modules in one host behind no gateway (MMCA.Helpdesk is the living single-host proof). Collapsing production would cut the$190-220/mo run cost to roughly a third and eliminate the gRPC partial-failure class (peer-not-ready, mixed-endpoint quirks, best-effort degradations) outright. Monthly cost drivers today: ACA ~$110-130 (6 apps, min 1 replica each), SQL ~$25 (5 Basic DBs), Log Analytics ~$25, Redis ~$13 (Balanced B0), Service Bus ~$10 (Standard). Blocker (deliberate): the distributed topology IS the GTM demonstrator (the sales program shows the framework's extract-a-service path running in production), so the collapse is deferred while that value outweighs the spend; the real 2026 load (76 accounts / ~67 peak) would be comfortably served by one host. Resolution path when revisited: single service host enabling all modules (Helpdesk pattern), oneADCdatabase via the resolver collapse (migrate the four DBs' data in), drop Service Bus/Redis/Gateway resources frommain.bicep, point the UI at the host directly, and re-run the k6 capacity proof at the collapsed tier. - Note: both Bicep templates validated locally with
az bicep build(clean). The new budget params default sensibly, so nodeploy.yml/main.parameters.jsonchange is required.
[x] #32 · Dependency & Supply-Chain Management · RESOLVED (single-axis 3 → 4; two-axis M3→4 / I7→8→9 as of the 2026-06-29 re-score); only a direct MassTransit pin remains for impl 10
- (Vulnerability scanning is active — NuGetAudit gates restore, which caught the MessagePack CVE; now also a blocking PR
supply-chainjob.) - Enable lock files, add an SBOM step, add license scanning. → DONE (TD-01 closed, 2026-06-26): 58 committed
packages.lock.json(RestorePackagesWithLockFile=trueinDirectory.Build.props:27; the Blazor WASM client + UI.Web host opt out viaRestorePackagesWithLockFile=false— sidestepping the NETSDK1124 trimming-check that wedged the earlier bootstrap), and thesupply-chainCI job's vuln-audit + SBOM are now blocking PR gates (deploy.yml:108-169,:146/:155exit 1, indeploy.needs); license/deprecated reports stay advisory. Residual (now keeps two-axis impl at 9, not 10): the--locked-modehalf is DONE (CI restore runs--locked-modein both gating jobs,deploy.yml:40/:119, so lock-file drift is tamper-enforced at restore, lifting scorecard §32 impl 8→9 on the 2026-06-29 re-score); the only remaining open sub-part is that MassTransit v8 is still pinned only transitively via MMCA.Common, not in ADC's own props.
[~] #33 · Developer Experience & Inner Loop · 3 → 4 (weight 2, priority (4-3)×2=2) · REOPENED 2026-07-15 (twentieth-cycle re-score): the wave-6 candidacy was REJECTED for both axes. The README half is genuinely done, but broker parity (local RabbitMQ vs prod Azure Service Bus) was mitigated, not closed, so scorecard §33 holds M3/I8 (a proposed impl 9 was also rejected on the same evidence). Re-confirmed M3/I8 on 2026-07-17 (twenty-first cycle) and again on 2026-07-21 (twenty-second cycle) on a rewritten basis: the README.md:74 quote this item hung on (Service-Bus-specific behavior "only observable in the deployed environment") no longer exists, since the emulator tier landed and README.md:80-84 now states the opposite. The tier is real (Tests/Integration/MMCA.ADC.ServiceBusEmulator.IntegrationTests, run as the servicebus-emulator-smoke job at .github/workflows/cross-service-tests.yml:123-146), but it runs nightly and reaches the deploy only through the cross-service-freshness recency gate (deploy.yml:610, in deploy.needs at :783), which is itself bypassable via skip_freshness_gates (:636). Nightly-plus-recency is not in-band, so the score holds at M3/I8; the M3→4 / I8→9 candidacy stands recorded for a future cycle
Thin onboarding (2-line README); manual PAT dependency; broker parity gap (local RabbitMQ vs prod Service Bus) still open.- Expand the onboarding README; document the
GITHUB_TOKENbootstrap → DONE:README.mdis now a full getting-started guide (prerequisites incl. Docker, theGITHUB_TOKENpackages:readbootstrap with the local-sourcelocal.propsalternative and the stale-Debug-DLL gotcha, run/test commands incl. MTP filter syntax, fixed local endpoints). - Close (not just record) the local-vs-prod broker parity gap → the gap is recorded, mitigated, and referenced (the README's parity section documents RabbitMQ-local vs Service-Bus-prod and points at the nightly Testcontainers broker round-trip whose recency gates deploys, TD-02), but closing it needs either a local Service Bus surface (e.g. the Service Bus emulator in the Aspire AppHost, or an opt-in cloud-broker local profile) or an automated Service-Bus-behavior test tier; documentation alone holds §33 at M3/I8. CLOSED 2026-07-16 via the automated Service-Bus-behavior test tier:
Tests/Integration/MMCA.ADC.ServiceBusEmulator.IntegrationTestsruns MassTransit v8 against the official Service Bus emulator (pinned 2.0.1, the first line with the admin plane MassTransit's topology provisioning needs) with ADC's REAL integration-event contracts, proving admin-plane topology creation + the AMQP publish-to-consume round-trip nightly incross-service-tests.yml(a new job in the same workflow, so its result rides the existingcross-service-freshnessdeploy gate). Design notes: MassTransit v8 has no vendor emulator mode (v9-only; excluded by the v8 policy pin), so the tier uses the public custom-clientsHost()overload, its own test process (the emulator's 1h TTL quota requires overriding process-global MassTransit defaults), and one warm container (10-connection + admin-throttle quotas). Deliberately a smoke, not a port of the 9 RabbitMQ round-trips: the RabbitMQ tier keeps the outbox/inbox pipeline coverage; this pins the transport. §33 M3→4 + I8→9 candidacy recorded for the next re-score.
Deliberate / accepted (recorded decisions, not scheduled work)
Conscious, recorded choices, not pending work (the former TECHDEBT.md accepted-risk section):
- Single-region deployment (no multi-region failover) — accepted in
infra/DISASTER-RECOVERY.md; the real load (~67 peak concurrent in 2026) doesn't justify the cost/complexity. - No conference-day
minReplicas:2— the 2026 load didn't warrant it; recorded as accepted risk ininfra/DISASTER-RECOVERY.md. The weeklycost-guard.ymlwould flag a surge that was applied and not reverted. - No interactive OpenAPI UI (Scalar/Swagger) — the h2c-only REST services aren't browser-reachable directly; a Gateway-routed UI is a small follow-up if/when wanted (#9).
- Legacy pre-cutover database retained — kept untouched (Basic tier) as the rollback/archive source; never written to after the per-service-DB cutover. Intentional.
- Integration events published post-commit carrying DB-generated IDs — intentional (the event must carry the persisted identity); not debt (#6).
- #1 SOLID —
AuthenticationService7-ctor-dependency cohesive auth facade — accepted as-is; the ctor-count fitness threshold (ConstructorDependencyCountTests, ≤7) is now landed on the v1.86.0 sweep, so #1 is closed (scorecard §1 stays M4/I9). - #5 Vertical Slice, deliberate layered-by-project hybrid: cross-cutting handled in the decorator pipeline; the hybrid is the accepted choice that caps implementation at 8, and the slice-cohesion line is held by a CI-gated fitness test (
SliceCohesionTests, inMMCA.ADC.CI.slnf) that lifted scorecard §5 to maturity 4 (#5 closed, scorecard §5 M4/I8). - #20 Design System Common-side residuals: accepted as out-of-ADC-scope:
BrandColorTokenTestsguards the Primary token only (Secondary has no drift test), and a few!importantoverrides + Store-specific cart CSS live in MMCA.Common's sharedapp.css. These are MMCA.Common changes, not ADC-local; ADC's §20 is maturity 4 / impl 9. - Chromium-only deploy E2E gate (recorded 2026-07-18, CI-minute reduction):
deploy.yml'se2e-gateinvokes one browser leg instead of three (deploy.yml:488, rationale comment at:478-480); firefox/webkit cross-engine coverage moved to the nightlye2e.ymlmatrix, wherecontinue-on-error(e2e.yml:119) keeps them advisory. Recorded as a deliberate cost choice, with its scoring consequence stated plainly: it costs §22 its maturity 4, so the category reopens at M3/I8 (see #22). This is a trade-off, not a closure; option (b) under #22 (across-browser-freshnessgate) would recover the maturity without restoring the runner minutes. - Freshness-gate break-glass: the three recency gates (
dr-freshness,load-freshness,cross-service-freshness) each accept askip_freshness_gatesworkflow_dispatch input with a required justification (deploy.yml:520,577,636), so every one of those proofs is bypassable by an operator. Recorded as an accepted escape hatch; it slightly qualifies the "enforced deploy precondition" language used under #6, #12, #29, and #33. - FLAG re-checks: This re-score's (v1.93.0 sweep) only FLAG is §7 (M4/I8, in protect): a proposed impl 8→9 lift was adversarially rejected, the bidirectional Conference↔Engagement gRPC pair caps it in the Strong band, so it is a verified non-move. The prior 2026-06-29 re-score's other re-checks have since settled: §5 was lifted to M4 on the v1.93.0 sweep (slice-cohesion CI gate, no longer flagged), while §25 (M4/I8, closed; route-auth fitness tests CI-gated) and §13 (M3/I8, open under Priority 2) are now plain CONFIRMED. A FLAG is a verified non-move, not a closure. Update (2026-07-03 full re-score): all 34 categories returned CONFIRMED with no new FLAGs; §7 remains the standing verified non-move (M4/I8: the bidirectional Conference↔Engagement gRPC pair caps it in the Strong band), and the §24 impl 9→7 recalibration is a tracked substance gap (TD-14), not an accepted trade-off, so it does not enter this section. Update (2026-07-10 nineteenth-cycle full re-score): the FLAG set shifted. §7 returns plain CONFIRMED (M4/I8, no longer flagged; the bidirectional gRPC pair is a settled cap). The three verified non-moves this cycle are: §12 (M3/I8: a proposed impl 8→9 was adversarially rejected because the Notification app stays pinned
maxReplicas: 1,infra/main.bicep:1113, while the backplane key is injected at:1056; the stale no-backplane bicep comment was corrected this cycle), §23 (M3/I8: a proposed maturity 3→4 was rejected; the WebVitals budgets are advisory by design, no §23 fitness gate exists, and the k6/vitals tiers run nightly/dispatch, not as a merge gate), and §34 (M4/I9: a proposed impl 9→8 downgrade was rejected as unsupported; the untracked workspace-rootArchitecturalAnalysis.mdremains the already-weighed 9-not-10 lever). Each is a verified non-move (score held), not a closure. Update (2026-07-15 twentieth-cycle full re-score): the FLAG set shifted again. §12 returns plain CONFIRMED (M3/I8, no longer flagged) and §23 exits as a lift (maturity 3→4 on the now-enforced CWV budgets, superseding its nineteenth-cycle rejection). This cycle's adversarial adjudications: §19 (a first-pass impl 9→8 downgrade was rejected as unsupported while the maturity 3→4 lift was confirmed, so §19 closes at M4/I9), §28 (M4/I8 verified non-move, but its row carried a materially false claim now corrected in place: E2E #5 is re-quarantined atSpeakerSelfServiceTests.cs:57, not "un-skipped/active", plus three drifted line anchors), §33 (M3/I8: a proposed impl 8→9 was rejected on the open broker-parity red flag,README.md:74), and §34 (M4/I9: the identical impl 9→8 downgrade re-proposed and re-rejected). Each non-move is a held score, not a closure. Update (2026-07-17 twenty-first-cycle full re-score): one FLAG this cycle: §27 (M4/I8 verified non-move: the recorded impl 8→9 candidacy, extending the pseudo-loc text-expansion evidence to ADC pages, was adversarially rejected becausePseudoLocalizationTests.cs:51covers only 3 public pages of 30+ routable pages, a partial extension; §27 stays in the protect set at its held score). §12 and §33 return plain CONFIRMED at M3/I8 (their twentieth-cycle adjudications re-derived from fresh evidence, including theload-freshnessgate and the Service Bus emulator tier, neither sufficient for a move). A FLAG is a held score, not a closure. Update (2026-07-21 twenty-second-cycle full re-score): the single FLAG is again §27 (M4/I8): the identical impl 8→9 pseudo-loc candidacy was re-proposed and re-rejected on unchanged evidence (PseudoLocalizationTests.cs:51covers exactly 3 public pages against 36 routable pages), so it stays a verified non-move in the protect set. The §33 sentence in earlier updates that quotedREADME.md:74is superseded: that admission no longer exists in the file (see the #33 header for the rewritten basis). Update (2026-07-23 twenty-third-cycle full re-score): the FLAG set shifted: §27 returns plain CONFIRMED (M4/I8, in the protect set; the impl 8→9 pseudo-loc candidacy was not re-proposed this cycle). The two verified non-moves are §12 (M3/I8: a proposed maturity 3→4 was adversarially rejected because the k6 capacity proof executes monthly/dispatch out of band withload-freshnessa recency-only check,deploy.yml:548, and Notification stays pinnedmaxReplicas: 1,infra/main.bicep:1424) and §21 (M3/I8: a proposed maturity 3→4 was rejected because the manual screen-reader pass is still unrecorded inACCESSIBILITY-SCREENREADER-PASS.md, the cheapest maturity 3→4 lever). §22 and §33 are plain CONFIRMED at M3/I8. A FLAG is a held score, not a closure.
✅ Already at level 4 — protect, don't regress
#1 SOLID · #2 Design Patterns · #3 Clean Architecture · #4 Domain-Driven Design · #5 Vertical Slice Architecture · #6 CQRS & Event-Driven · #7 Microservices Readiness · #8 Data Architecture · #9 API & Contract Design · #10 Cross-Cutting Concerns · #11 Security · #13 Observability & Operability · #14 Testability & Test Strategy · #15 Best Practices & Code Quality · #16 Maintainability & Evolvability · #17 DevOps & Deployment · #18 UI Architecture & Components · #19 State Management & Data Flow · #20 Design System · #23 Front-End Performance · #24 Forms & UX Safety · #25 Navigation & Information Arch · #26 Front-End Security · #27 Internationalization · #28 Front-End Testing & Quality · #29 Resilience & Business Continuity · #30 Compliance & Privacy · #31 Cost Efficiency / FinOps · #32 Dependency & Supply-Chain · #34 Architecture Governance & Docs (30 categories at maturity 4)
(The pattern/layer/governance categories are auto-enforced by the architecture fitness functions in the deploy gate; the rest reached maturity 4 via the remediation tracked above. Keeping those gates green is the regression guard. UPDATE 2026-06-30: §16/§24/§27/§29/§31 joined the protect set via the enforcement-gate wave: #24/#16/#27 by new CI.slnf fitness tests, #31/#29 by the cost-guard/dr-freshness deploy.needs gates (committed, activate on the next push). The 2026-06-29 §29 reopening is superseded. UPDATE (v1.93.0 sweep, 2026-06-30): #5 Vertical Slice Architecture also joined the protect set, its slice-cohesion fitness test confirmed a CI merge gate in CI.slnf. UPDATE (2026-07-02 re-score): #18 UI Architecture left the protect set because scorecard §18 maturity was corrected 4→3 (no automated §18 UI-architecture fitness gate; the container/presentational + code-behind conventions are review-enforced only), so it is reopened as an active priority-3 item and the count is now 26. UPDATE (2026-07-03 reconciliation): #28 Front-End Testing joined the protect set (scorecard §28 maturity 4 via the deploy-gating chromium e2e-gate) and #19 State Management left it (scorecard §19 maturity corrected 4→3 on the fifteenth cycle: no §19 fitness gate), so the membership swapped and the count stays 26. UPDATE (2026-07-15 twentieth-cycle re-score): #18 UI Architecture, #19 State Management, and #23 Front-End Performance joined the protect set (the §18/§19 fitness gates now run in the CI.slnf arch gate and the §23 CWV budgets are enforced inside the deploy-gating e2e-gate), taking the count to 29. UPDATE (2026-07-17 twenty-first-cycle re-score): #13 Observability and #22 Responsive & Cross-Browser joined the protect set (the ObservabilityConventionTests alert-runbook pairing gate runs in the CI.slnf arch gate, and all three e2e-gate browser legs now block the deploy per e2e.yml:78), taking the count to 31. UPDATE (2026-07-21 twenty-second-cycle re-score): #22 Responsive & Cross-Browser LEFT the protect set (scorecard §22 maturity corrected 4→3: the 2026-07-18 Actions-minute reduction cut the deploy e2e-gate to chromium only, deploy.yml:488, leaving firefox/webkit nightly-advisory under e2e.yml:119), taking the count to 30. #18 stays in the protect set: its maturity 4 gate is intact and only its implementation moved 9→8 (TD-16). The maturity-4 set is exactly the 30 categories other than §12/§21/§22/§33.)
Suggested sequencing — updated 2026-06-11
- ✅ Tokens out of
localStorage+ CSP (#26) — cookie-only refresh + OAuth code-exchange + enforced CSP shipped. (Residuals: Gateway headers; the Option-A-vs-C+ BFF decision is pending the user.) - ✅ Rework the orphaned integration tier (#14) — per-service WAF tiers, ~345 tests, deploy-gated.
- ✅ Real erasure path + stop logging PII (#30) —
IAnonymizable+ anonymize-on-delete + export endpoint + log redaction. (Residual: cross-service export aggregation.) - ✅
UnsavedChangesGuardsweep (#19 + #24) and admin-route authz (#25). - ✅ bUnit + axe harness, E2E as a merge gate (#28 + #18 + #21): the bUnit tier shipped earlier; the chromium E2E/axe suite became the deploy-gating
e2e-gateon 2026-07-02 (#28 closed, TD-06/07 done). (Residuals: the #18/#19 UI fitness gates and the #21 recorded SR pass.) - Credential hardening (#11 rate-limiter [Common] + #17 Key Vault/managed identity) and observability (#13/#29 alerts, RTO/RPO, LTR backups), then doc/CLAUDE.md drift (#9, #34).
Current top levers (2026-06-30, after the enforcement-gate wave): the five "good-but-not-a-gate" maturity items that were the prior top levers (#16/#24/#27/#29/#31) are now closed by CI-enforced gates. The remaining OPEN levers are the front-end-E2E cluster, all gated by one blocker: #21 Accessibility (priority 6, the single highest-leverage open item: the SR pass is recordable now to reach maturity 2→3, but the axe merge gate for maturity→4 is blocked), #28 (promote E2E/axe to a merge gate), and #22 (cross-browser pass). All three are blocked by the same diagnosed Blazor-Server-under-load E2E limit (see the #28 root-cause note), so the gate path is a slow-pace or dedicated-CPU runner, not another test fix. The cheapest open win is the recorded manual screen-reader pass (#21 maturity 2→3, ACCESSIBILITY-SCREENREADER-PASS.md), which needs a human + NVDA/VoiceOver against the running Aspire app.
Defect-fix wave, 2026-07-05 (A-1..A-7, cross-repo defect audit)
Targeted correctness wave; every behavior change flipped its pinning test in the same commit.
- A-1 Cancellation no longer swallowed:
AnthropicScoringService.ScoreSessionAsyncand theScoreEventSessionsHandlerpersistence catch now filterwhen (ex is not OperationCanceledException)(repo idiom, cf.UserRegisteredHandler); the service's "never throws" doc is scoped to scoring failures, cancellation propagates. - A-2 Partial score JSON rejected: the seven
AiScoreResponsesub-scores are nullable; any missing one returns the failed-result shape instead of defaulting to 0 and clamping up to 1.0. Out-of-range clamping for present values is unchanged;reasoningstays optional. - A-3 (doc-only) Speaker-overlap docs corrected:
GetSpeakerSessionOverlapHandler,SpeakerSessionOverlapDTO/MultiSessionSpeakerdocs, and the pinning-test comment now state the handler intentionally returns EVERY speaker with a submitted session (the UI shows all speakers with a session-count column), sorted so multi-session speakers surface first. No behavior change; types not renamed. - A-4 Category-distribution soft-delete drift fixed:
GetCategoryDistributionHandler's category-existence predicate aligned withGetSessionSelectionDashboardHandler(!c.IsDeletedplus live-item count check), so a category whose only referenced item is soft-deleted is omitted entirely. - A-5 Duplicate guards added:
Session.AddSessionCategoryItemandSpeaker.AddSpeakerCategoryItemnow reject a live duplicate association (codesSession.CategoryItem.Duplicate/Speaker.CategoryItem.Duplicate), mirroringAddSessionSpeaker; re-add after soft-delete still succeeds. Verified both Sessionize sync strategies pre-filter live duplicates before calling Add, so re-imports are unaffected. - A-6 GDPR role check case-sensitivity (mirror of the Store fix):
DeleteUserHandler/ExportUserDataHandlercompared the raw role claim string ordinally againstUserRole.Organizer(via the implicit string conversion), denying organizers whose claim carried different casing. New case-insensitiveUserRole.IsOrganizer(string?)helper used in both, with lowercase-claim regression tests. - A-7 (cosmetic):
SessionLookupServicedropped the misleadingpageSize=10000query param: the base/sessionsendpoint has no pageSize parameter and always serves one page capped at MaxPageSize (500), so this was a verified non-bug (comment added noting the cap);SpeakerDashboardServicenotes the same cap; the staleMMCA.ADC.slnxcomment claiming the deleted combinedMMCA.ADC.IntegrationTestsproject "stays excluded pending re-home" was corrected (the folder is gone; the per-service projects are the integration tier).
Current top levers (2026-07-03, after the e2e-gate promotion and the sixteenth-cycle full re-score): the former Blazor-Server-under-load blocker is resolved for the gate itself (the E2E_FORCE_SERVER pin + reload-and-rewait fixes; chromium E2E/axe now gates every deploy, #28 closed, TD-06/TD-07 done). The open set is now priority 3: #18, #19, #21 and priority 2: #12, #13, #22, #23, #33. The cheapest win remains the recorded manual screen-reader pass (#21 maturity 3→4, needs a human). The one structural play is the paired §18 + §19 UI fitness gates (one arch-test wave reaches Optimized/M4 on both, subsuming TD-13). #22 waits on a reliably green firefox/webkit soak before gating the non-chromium legs. New this cycle: TD-14 under #24 (forms error-presentation substance, the §24 impl 7→8/9 lever).
Update 2026-07-06 (eighteenth-cycle full re-score, pin v1.106.0): no scores moved. TD-03 closed (#8 optimistic-concurrency round-trip now implemented and deploy-gated, Conference-only so §8 holds impl 9) and TD-02 partially addressed (the genuine broker round-trip test landed as the non-gating MMCA.ADC.CrossService.IntegrationTests; gating it plus enabling the inbox on all 4 services is the §6 impl 9→10 lever). The open maturity-3 set (§12/§13/§18/§19/§21/§22/§23/§33) is unchanged, and the cheapest win remains the recorded manual screen-reader pass (#21 maturity 3→4). Evidence counts refreshed (arch-tests 23/25/74, §14 unit 1507/223 + integration 303 gating / four tiers + 9 non-gating CrossService, coverage floor 38→55.5%, ADR set 001-038, §27 resx 40+40).
Correction 2026-07-17: a paragraph formerly here, labeled "Update 2026-07-12 (twentieth-cycle full re-score, pin v1.115.0, HEAD 0c9507b8)", was a stale draft from the superseded nineteenth-cycle working diff, accidentally committed via PR #15 (whose subject was the §31 Log Analytics ingestion cap). The actual twentieth-cycle re-score is 2026-07-15 / pin v1.116.0 (recorded in the Index note above); it did NOT close #12 (the §12 maturity candidacy was rejected and §12 held M3/I8), and its arch-test/ADR counts differed from the draft's. The same stale hunk had also overwritten the #12 header ("RESOLVED M4/I8") and two scorecard prose blocks (a "§12 mat 4" strength claim and a risk-1 rewrite asserting the firefox/webkit e2e-gate legs cannot fail the deploy, describing the pre-2026-07-16 e2e.yml); all are corrected in this cycle's pass.
Update 2026-07-17 (twenty-first-cycle full re-score, pin v1.117.0, HEAD c4c01aa5): two scores moved up, both maturity, on the 2026-07-16 gates. #13 and #22 closed to maturity 4 (protect set now 31): #13 on the ADC-local ObservabilityConventionTests alert-runbook pairing gate in the CI.slnf arch gate, #22 on the fully gating three-browser e2e-gate (e2e.yml:78 scopes continue-on-error to scheduled nightly non-chromium legs). The open below-4 set shrank to §12/§21/§33: #21 (the recorded manual screen-reader pass remains the cheapest maturity 3→4 win, needs a human + NVDA/VoiceOver; the Warning-outlined-alert AA-contrast item was FIXED 2026-07-16), #12 (M3/I8 re-confirmed: the k6 tier executes monthly/dispatch out of band; Notification pinned maxReplicas: 1), #33 (M3/I8 re-confirmed; the Service Bus emulator tier candidacy stands for a future cycle). One candidacy adversarially rejected as a verified non-move: #27 impl 8→9 (pseudo-loc coverage is 3 public pages of 30+, partial). Evidence counts refreshed (arch-tests 26 classes / 28 files / 82 methods, 3 ADC-local, re-run green this cycle; ADR set 001-048, pin v1.117.0, 15 packages; indices Maturity 97.8% (313/320) / Implementation 86.3% (690/800)).
Update 2026-07-21 (twenty-second-cycle full re-score, pin v1.121.0, HEAD 8509a05d): two scores moved down, neither on a code-quality regression, and the protect set drops to 30. #22 REOPENED at M3/I8 (priority (4-3)x2=2): the 2026-07-18 Actions-minute reduction cut the deploy e2e-gate to browsers: '["chromium"]' (deploy.yml:478-480,488), so firefox/webkit run only on the weeknight nightly matrix where e2e.yml:119 keeps them continue-on-error. The cheapest recovery is option (b) under #22: a cross-browser-freshness job in deploy.needs mirroring the dr / load / cross-service pattern (deploy.yml:496,553,610), which restores an enforced signal at near-zero runner minutes. #18 implementation 9→8 with maturity held at 4, tracked as new TD-16 (effort S): HappeningNow.razor.cs is flush at the enforced 400-line cap with zero headroom and six more files sit 360-379, so sub-component extraction per the TD-13 pattern is the impl 8→9 lever. Open below-4 set: §12/§21/§22/§33. #21 (the recorded manual screen-reader pass, still the cheapest maturity 3→4 win, needs a human + NVDA/VoiceOver) remains the highest-priority item at 3; #12/#22/#33 sit at priority 2. #12 banked its Redis-provisioning sub-item (infra/main.bicep:740) but stays open on the maxReplicas: 1 Notification pin (:1424); #33 keeps its score on a rewritten basis after its README.md:74 quote was found deleted. One candidacy rejected for a second cycle: #27 impl 8→9 (pseudo-loc covers 3 public pages of 36 routable). Indices Maturity 97.2% (311/320) / Implementation 85.9% (687/800), pin v1.121.0, ADR set 001-050.
Update 2026-07-23 (twenty-third-cycle full re-score, pin v1.123.0, HEAD 160f59f5): no scores moved and the ledger is unchanged: no closures, no new items, no re-ranks, and every TD sub-item status holds. The open below-4 set stays §12/§21/§22/§33 (#21 at priority 3, #12/#22/#33 at priority 2). Two proposed maturity lifts were adversarially rejected as verified non-moves: #12 (the k6 tier runs monthly/dispatch out of band, load-test.yml:8; load-freshness is a recency-only deploy check, deploy.yml:548; Notification pinned maxReplicas: 1, infra/main.bicep:1424) and #21 (the recorded manual screen-reader pass is still the empty placeholder in ACCESSIBILITY-SCREENREADER-PASS.md, needs a human + NVDA/VoiceOver; the 18-page chromium axe/E2E deploy gate re-confirmed active, deploy.yml:791). The v1.122.0/v1.123.0 lockstep sweeps (15 packages) moved no score. Indices hold Maturity 97.2% (311/320) / Implementation 85.9% (687/800), pin v1.123.0, ADR set 001-051.