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.md tactical register is folded in here (2026-06-26): each deferred sub-item keeps its TD-NN ID and lives under its #NN category 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-RESOLVED rows (#11, #14, #26, #29, #30, #32, …) have moved the real total well past it. For the current, authoritative scores use the canonical, in-repo ArchitectureScorecard.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: the dr-drill.yml cron 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-mode in 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: #24 FormsConventionTests, #27 TranslationCompletenessTests, and #16 FrameworkVersionConsistencyTests run in the CI.slnf arch gate (locally verified green, 74/74 arch tests pass); #31 cost-guard and #29 dr-freshness are wired into deploy.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 function SliceCohesionTests is confirmed a CI merge gate in MMCA.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% (HEAD 89d8439, 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: Conference appsettings.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, HEAD ac43c8d8, 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-308 e2e-gate job + :343 in deploy.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 in Tests/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 on FormsConventionTests). 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 new LocalizedTextConventionTests, 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, HEAD 8fc9e0d2, 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:16 carries the RowVersion token via IConcurrencyAware, UpdateEventHandler.cs:34 stamps it with SetOriginalRowVersion, OrganizerConcurrencyTests.cs:26 asserts a stale token returns 409 inside the deploy-gating MMCA.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 nightly MMCA.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, HEAD 246a24dc, 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 pinned maxReplicas: 1, infra/main.bicep:1113, even though the v1.110.0 wave provisioned Azure Managed Redis Balanced B0 and scaled the REST services to maxReplicas: 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-root ArchitecturalAnalysis.md remains 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 contradictory main.bicep Notification 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, HEAD 913d088a, five scores up): the remediation-wave candidacies recorded below were adjudicated. Accepted: #18 CLOSED (scorecard §18 maturity 3→4: UIArchitectureConventionTests in the CI.slnf arch gate), #19 CLOSED (scorecard §19 maturity 3→4: StateManagementConventionTests in 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 chromium e2e-gate on 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.md day-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 run continue-on-error: true per e2e.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, per README.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 at SpeakerSelfServiceTests.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, HEAD c4c01aa5, two scores up): the two 2026-07-16 gate candidacies were adjudicated ACCEPTED. #13 CLOSED (scorecard §13 maturity 3→4: ObservabilityConventionTests machine-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-gating e2e-gate passes all three engines, deploy.yml:309, and e2e.yml:78 scopes continue-on-error to 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:51 covers 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 via load-freshness, deploy.yml:348,417, but executes monthly/dispatch out of band, and Notification stays pinned maxReplicas: 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, HEAD 8509a05d, two scores down, neither a quality regression): #22 REOPENED (scorecard §22 maturity 4→3: the 2026-07-18 Actions-minute reduction cut the deploy e2e-gate to browsers: '["chromium"]', deploy.yml:488 with its rationale comment at :478-480, so firefox/webkit run only on the weeknight nightly schedule where e2e.yml:119 keeps them continue-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 the UIArchitectureConventionTests gate, but the largest code-behind sits flush at the enforced 400-line cap with zero headroom, HappeningNow.razor.cs:400 vs UIArchitectureConventionTestsBase.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:51 still 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 the maxReplicas: 1 pin (:1424), so #12 itself stays open. Corrected: #33's load-bearing README.md:74 quote no longer exists (the file now states the opposite at README.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, HEAD 160f59f5, 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, with load-freshness a recency-only deploy check, deploy.yml:548, and Notification pinned maxReplicas: 1, infra/main.bicep:1424) and §21 M3→4 rejected (the recorded manual screen-reader pass is still the empty placeholder in ACCESSIBILITY-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 tierDONE (#14): per-service WebApplicationFactory tiers, ~345 tests gating every deploy — also closed #11's authz-gate and #16's non-building projects, and advanced #8. See IntegrationTestReworkPlan.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 onto SqlServerIntegrationTestFixtureBase. A new Notification integration project (SignalR hub + inbox) closed the last untested service. The deferred Phase 4 broker-transport tier landed as MMCA.ADC.CrossService.IntegrationTests (Testcontainers RabbitMQ + SQL, non-gating nightly cross-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).
  • UnsavedChangesGuard sweepDONE: fixed #19 (6 create forms) and #24 (6 inline-edit paths); both categories are now closed at maturity 4.
  • 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 localStorageIMPLEMENTED (cookie-only refresh; pending manual Aspire verification + release): localStorage is gone; the refresh token lives only in the HttpOnly cookie and is exchanged server-side (/auth/session/token + UseCookieSessionRefresh + ICookieSessionRefresher in 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. See TokenStorageDesignNote.md.
  • (High) OAuth completion redirect carries both tokens in the URL query stringRESOLVED (Wave 1, item ①): OAuthController.CompleteAsync now mints a single-use code, stashes the token pair in the cache, and redirects with only ?code=…; the UI redeems it via POST auth/oauth/exchange (OAuthController.ExchangeAsync). Tokens no longer touch the URL, history, Referer, or access logs.
  • (Medium) No CSP or security headersRESOLVED (UI host): SecurityHeadersMiddleware sets nosniff / X-Frame-Options: DENY / Referrer-Policy / Permissions-Policy, plus a full CSP now enforced with connect-src pinned 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): GatewaySecurityHeadersMiddleware adds nosniff / X-Frame-Options / Referrer-Policy / Permissions-Policy / CSP frame-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 by OAuthControllerTests (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-src pinned (BlazorCspPolicyProvider); the Gateway sets the hardened headers on every response via the shared AddCommonSecurityHeaders/UseCommonSecurityHeaders middleware registered first in its pipeline (Source/Hosts/MMCA.ADC.Gateway/Program.cs:31,61). (The line-31 audit note above mentioned a bespoke GatewaySecurityHeadersMiddleware; 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/SecurityHeadersTests boots the real Gateway via WebApplicationFactory<Program> (no SQL — runs in the fast CI tier / CI.slnf) and asserts /alive carries X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy, Permissions-Policy, CSP frame-ancestors 'none', and HSTS (Production env). A refactor dropping UseCommonSecurityHeaders() now fails CI.
  • Shorten the 30-day refresh cookie; consider SameSite=Strict. → DONE (2026-06-27, with recorded SameSite decision): the session/refresh cookie is already 7 days (not 30) — SessionCookieJar (MMCA.Common.API) pins Lifetime = TimeSpan.FromDays(7), "aligned to the refresh-token lifetime so a cookie never outlives the credential it carries." SameSite=Strict is deliberately NOT adopted: SameSite=Lax is 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/token endpoint's Sec-Fetch-Site check + 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-48 runs only CI.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), in CI.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 inject AuthenticationStateProvider directly) — Profile loaded/error-state bUnit tests + the /users authz fitness test; and Engagement.UI.Tests covering both feedback formsEventFeedbackTests (dynamic question render by type + per-question upsert skipping unanswered) and now SessionFeedbackTests (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 bUnitDataGridListPageBase is 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 testManagementRouteAuthorizationTests (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-core AccessibilityTests (17 pages, Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/AccessibilityTests.cs) run inside the deploy-gating chromium e2e-gate job (e2e.yml:236 runs the whole E2E project; deploy.yml:343 puts e2e-gate in deploy.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-gate job (deploy.yml:303-308 uses: ./.github/workflows/e2e.yml with browsers='["chromium"]'; e2e.yml:31 workflow_call), promoted after validation run 28604877733 (first fully green three-browser matrix). The former Blazor-Server-under-load blocker was resolved by the E2E_FORCE_SERVER pin + 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:ForceWebAssemblyApp.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 noteinfra/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 (serviceDatabaseLtr in main.bicep); PITR is already geo-redundant (Basic default). Restore drill automated (one-click dr-drill.yml + scripts/dr-restore-drill.ps1) and executed end-to-end 2026-06-20: PITR restore of ADC_Conference into a throwaway copy in 2.6 min (vs 2 h RTO), verified Online, cleaned up; row recorded in DISASTER-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 (sloWorkbook in main.bicepworkbooks/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-freshness job was added to deploy.yml and to the deploy job's needs. It fails the deploy unless the latest dr-drill.yml run concluded success within an 8-day freshness window (covering the weekly cron: '0 6 * * 1'), via one gh api Actions 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 on dr-drill.yml's weekly cron; GracefulShutdownTests remains 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.bicep now provisions an action group + three App-Insights metric alerts (failed requests, server response time, dependency failures), email via the ALERT_EMAIL repo variable.
  • Consider minReplicas:2 for the gateway/UI on conference day only. Deliberately deferred — 2026 load (~76 acct) didn't warrant it; recorded as accepted risk in DISASTER-RECOVERY.md.
  • 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 as MMCA.ADC.Gateway.Tests/GracefulShutdownTests — it boots the real Gateway host via WebApplicationFactory<Program>, requests a stop under a bounded 20s token, and asserts IHost.StopAsync drains and completes (the host reaches ApplicationStoppingApplicationStopped) within the timeout; a hosted service that refused to drain would cancel the token and fail the test. Headless, in CI.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 — User.Delete() retains email, name, password hash/salt, device metadata, OAuth keys indefinitely.RESOLVED: User now implements the framework IAnonymizable seam (v1.53.0); User.Anonymize() irreversibly overwrites email (→ unique deleted-{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). DeleteUserHandler calls it on every deletion request, so erasure is immediate — well inside the PRIVACY.md §5 "30 days" promise. Covered by UserAnonymizeTests (3 domain tests); DeleteUserHandlerTests green.
  • (Medium) PII (email + first/last name) written to App Insights traces with no redaction. UserRegisteredHandler.cs:179-198.RESOLVED: the four PII-bearing LoggerMessage templates (email ×3, name ×1) now log only the stable {UserId}/counts — no email or name reaches the trace pipeline (matches PRIVACY.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 portable UserDataExportDTO (email, name, role, login provider, device metadata, speaker link, timestamps), excluding credentials (hash/salt, refresh token, provider key). Covered by ExportUserDataHandlerTests. Cross-service aggregation (Engagement bookmarks, Notification messages) for full §7 coverage remains.

Fix

  • Implement a real erasure pathIAnonymizable + 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 piececross-service aggregation DONE 2026-07-11 (remediation wave 6): the export now aggregates Engagement (session bookmarks + submitted live-Q&A questions, new user_engagement_export.proto rpc mirroring the bookmark-count pattern) and Notification (inbox items, new user_notification_export.proto rpc on the existing ADR-012 grpc ingress; a new Notification.Shared layer 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 (AppHost WithReference without deadlocking WaitFor; 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 new PiiLogCapture sink in the test factory, asserted against unique markers). §30 I9→10 candidacy already recorded stands on stronger evidence.
  • (Low) Hardcoded user-facing English throughout markup, e.g. Source/Modules/Conference/.../Pages/Speaker/SpeakerDashboard.razor:7-60; no .resx, no IStringLocalizer, InvariantCulture display. RESOLVED (v1.86.0 sweep, 2026-06-27): ADC now ships real en-US + es i18n (36 base .resx + 35 .es.resx across the three module UIs + three API error-resource sets, IStringLocalizer<T> in ~33 pages, culture-aligned SSR/Server/WASM, cross-device User.PreferredCulture persistence, backend error localization keyed on Error.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 gateDONE 2026-06-30: Tests/Architecture/MMCA.ADC.Architecture.Tests/TranslationCompletenessTests.cs pairs every base .resx under Source/ with an .es.resx sibling 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 polishDONE 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 new LocalizedTextConventionTests gate 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.cs extends the pseudo-loc evidence to ADC's own public pages (/, /conference/events, /conference/sessions): activates qps-Ploc via the app's own /culture/set endpoint (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:51 covers 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.

🟡 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 MMCA.ADC.WebAPI host, won't build, and is excluded.RESOLVED: reworked as per-service WebApplicationFactory<Program> tiers (Identity/Conference/Engagement, ~345 tests) over a SQL-service CI container, plus the revived MMCA.Common.API middleware unit tests. In-process JWT override (for AddForwardedJwtBearer), gRPC fakes, broker InProcess short-circuit, Respawn reset. Runs via MMCA.ADC.Integration.slnf and gates every deploy (integration-tests job in deploy.yml, a required dep of deploy). 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.*;-*.Tests code, ratcheted to 55.5 after the 2026-07 coverage program, actual ~57%) that hard-fails the deploy-gating build-and-test PR 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 SQL integration-tests job): Conference.IntegrationTests/CrossService/CrossServiceUserRegisteredTests.cs (BR-207 name-match auto-link / ambiguous-skip / no-match-skip) + Identity.IntegrationTests/CrossService/CrossServiceSpeakerLinkTests.cs (SpeakerLinkedToUser/SpeakerUnlinkedFromUser set/clear User.LinkedSpeakerId). Pairs with OutboxFidelityTests (which covered the producer side only). Added via a small additive Services accessor 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) on cross-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 GlobalLimiter/[EnableRateLimiting].RESOLVED: MMCA.Common 1.54.0's AddCommonRateLimiting now attaches a GlobalLimiter (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/grpc bypassed. 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 via keyVaultUrl + 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 UnsavedChangesGuard param-lag... spurious "unsaved changes" prompt after a successful createRESOLVED: all six Conference create forms now pass the framework live-accessor IsDirtyAccessor="() => _isDirty". The MMCA.Common UnsavedChangesGuard live-accessor (shipped v1.51.0) reads dirty state at navigation time, eliminating the one-render parameter lag — no StateHasChanged()-before-NavigateTo dance needed.

Fix

  • Adopt the framework live-accessor guard (MMCA.Common #19, shipped v1.51.0) on all six create forms; supersedes the StateHasChanged()-before-NavigateTo workaround.
  • (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.0 StateManagementConventionTestsBase) reflects over the three module UI assemblies (registered as Layer.Ui in AdcArchitectureMap) failing the build on any mutable static field or settable static property, plus a source scan forbidding singleton *StateService/*StateContainer registrations. 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.cs runs axe-core WCAG 2.1 AA scans (broadened to 17 pages on 2026-06-30); the target is stated in CLAUDE.md and ACCESSIBILITY-SCREENREADER-PASS.md. (Deploy-gated since 2026-07-02: the scans ride the chromium e2e-gate job in deploy.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 to Severity.Info outlined). ADC carries the same latent pattern in at least SpeakerDashboard.razor:37 and SessionFeedback.razor:29 (plus amber Variant.Outlined Color.Warning buttons on EventDetail.razor:141 and the bookmarked-state toggle on PublicSessionDetail.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 to Severity.Info outlined (Store parity; the sweep also caught PresenterView.razor:21 and SessionLive.razor:21), and the two outlined amber buttons moved to the AA-passing Secondary teal (EventDetail Unpublish, and the bookmarked state of PublicSessionDetail'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-gate in deploy.needs after 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.0 UIArchitectureConventionTestsBase) caps every *.razor.cs under Source/ at 400 lines and inline @code blocks at 120 lines. Verified non-vacuous via a seeded 402-line file. Subsumed TD-13 (below) and additionally forced conforming splits of SessionLive.razor.cs 648→357 (three extracted panels) and PublicSessionList.razor.cs 499→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.cs 507→367 (extracted SessionSelectionSpeakerOverlap, SessionSelectionAiScores, and the pure-rules SessionSelectionDisplay helper) and SpeakerDetail.razor.cs 429→368 (extracted SpeakerCategoryItemsPanel). 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.cs is exactly 400 lines against the MaxCodeBehindLines => 400 cap (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 gatebuild-and-test now runs dotnet ef migrations has-pending-model-changes for 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 testSoftDeleteFidelityTests (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 with IsDeleted = 1 (soft- not hard-delete). The fixture now exposes its ConnectionString for raw-table assertions.
  • Outbox-dispatch fidelityOutboxFidelityTests (Identity tier) registers a user and asserts a UserRegistered row landed in [dbo].[OutboxMessages] (confirmed InProcessEventBus.PublishAsync persists the row transactionally, then marks it processed — the row is retained). The Identity fixture now exposes ConnectionString. (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 EventDTO carries the RowVersion token via IConcurrencyAware (Conference.Shared/Events/EventDTO.cs:16), UpdateEventHandler.cs:34 stamps the client's last-seen token with SetOriginalRowVersion (a stale token then raises DbUpdateConcurrencyException, which DbUpdateExceptionHandler maps to 409), and OrganizerConcurrencyTests.cs:26 (Update_WithStaleRowVersion_ReturnsConflict) asserts the 409 inside the deploy-gating MMCA.ADC.Integration.slnf (the integration-tests job is in deploy.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 (SetOriginalRowVersion on 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) AuthenticationService has 7 constructor dependencies (down from 9 — validators bundled into AuthenticationValidators) and injects a command handler directly. Source/Modules/Identity/.../Users/AuthenticationService.cs:21-28. 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: AuthenticationService sits 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.cs caps constructor dependencies at ≤7, with AuthenticationService at 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-in Microsoft.AspNetCore.OpenApi, package wired via the .Service convention in Directory.Build.props). Mapped outside Production only — these are internal services reached through the Gateway, which does not route the endpoint. The ApiExplorer group ('v'VVVv1) 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.json document).
  • Contract test (OpenApiContractTests in MMCA.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 /ServiceInfo at v1.0 (deprecated) and v2.0, selected by the api-version header — exercising MapToApiVersion routing + deprecation reporting (ReportApiVersions). ApiVersioningTests (integration tier) asserts each version returns its own shape and that the api-supported-versions / api-deprecated-versions headers 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 MessageBus__Provider=AzureServiceBus in prod — ✅ fixed 2026-06-08 (CLAUDE.md broker note corrected; provider switch + Standard-tier/Manage gotchas documented).
  • Remaining: 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 ADRADRs/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 gatepost-deploy smoke gate + auto-rollback added (deploy.yml Phase 5: Gateway /health + JWKS + UI probes → az containerapp revision copy rollback on failure; documented in infra/DISASTER-RECOVERY.md).
  • Switch runtime auth to managed identityDONE: 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=true activated in prod via the staged infra/SQL-MANAGED-IDENTITY.md sequence (deploy.yml repo-var passthrough → Entra admin → per-DB CREATE USER ... FROM EXTERNAL PROVIDER + db_owner → flag flip). All four services run passwordless on Authentication=Active Directory Managed Identity with as db_owner in 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 (with IsDirtyAccessor) + MarkDirty/_isDirty dirty-tracking added to all six Detail edit forms (Event/Speaker/Room/Session/Question/ConferenceCategory); _isDirty resets 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 summaryRESOLVED (v1.86.0 sweep, 2026-06-27): Identity.UI/Pages/Profile/Profile.razor:29,33,37 adds Required + RequiredError to all three fields, :38 wires client-side match (ValidateConfirmPassword) alongside ValidateNewPassword, and :41-52 renders a per-form MudAlert error 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.cs scans the six Conference *Create.razor forms and fails the build if any drops its UnsavedChangesGuard (with a live IsDirtyAccessor), _isDirty tracking, validated <MudForm, or Required/RequiredError markers (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 MudAlert error summary the Profile form pioneered (localized Validation.CorrectFollowing heading + the _form.Errors list, en+es key pairs added to all six form resx pairs; the snackbar kept as the secondary channel). (b) FormsConventionTests now covers the Profile form via a dedicated fact (error summary + ValidateNewPassword/ValidateConfirmPassword wiring + the three Required password fields) AND hardens the create-form gate by appending the error-summary markers to RequiredMarkers, 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_RendersThePerFormErrorSummaryWithItems renders the form, fails validation, and asserts the summary MudAlert actually 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 (sloWorkbook Azure Monitor workbook in main.bicepworkbooks/adc-slo-workbook.json, mirroring the SLO alerts per service).
  • Day-2 operational runbooks DONE (2026-07-11): infra/OPERATIONS.md maps 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-sloAlertSpecs governance 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.cs machine-enforces the alert-to-runbook pairing in the CI.slnf arch gate (runs on every PR and gates deploy): every sloAlertSpecs entry in infra/main.bicep must keep a ### ...-alert-<key> section in infra/OPERATIONS.md whose 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. The OPERATIONS.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's e2e-gate was cut to browsers: '["chromium"]' on 2026-07-18 for Actions-minute savings (deploy.yml:478-480,488), so firefox/webkit now run only on the nightly schedule, where e2e.yml:119 keeps them continue-on-error. Options: (a) re-add the two legs to the deploy gate (3x runner minutes, the 2026-07-16 shape), (b) add a cross-browser-freshness job to deploy.needs mirroring 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 ([Authorize] only, no role attribute).RESOLVED: the 18 master-data management routes now carry @attribute [Microsoft.AspNetCore.Authorization.Authorize(Roles = "Organizer")] — Event/Session/Room/Question/ConferenceCategory (list+create+detail), Speaker (list+create), and Identity UserList (/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 Identity Profile/UserClaims are self/attendee-facing.
  • Add role-based authorization ([Authorize(Roles)]) to admin routes.
  • Residual: DONEManagementRouteAuthorizationTests (reflection fitness test in Conference.UI.Tests) asserts every admin-namespace page keeps [Authorize(Roles="Organizer")] so a route can't silently drop to bare [Authorize]. Identity UserList (/users) is covered by the parallel IdentityRouteAuthorizationTests in the new Identity.UI.Tests project.

[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 guardIntegrationEventContractTests (architecture tier) reflects over every IIntegrationEvent in 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=true in Identity.Service + Conference.Service appsettings (the two services that consume integration events; each already ships the InboxMessages table via its AddInboxMessages migration). Dedup is now verified in MMCA.Common by EfInboxStoreTests (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-freshness job in deploy.yml's needs fails a deploy when the latest successful nightly cross-service-tests.yml run 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=true on all four consumer services: Engagement and Notification appsettings joined Conference + Identity (their InboxMessages tables shipped with the 2026-06-09 AddInboxMessages migrations, 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 testingDONE: the k6 conference-read-load.js load 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 pinned maxReplicas: 1, main.bicep:1007-1012).
  • (impl-8 lever) Add client-side Core Web Vitals measurement to the E2E suiteDONE 2026-06-30: a WebVitalsTests Playwright tier (Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/WebVitalsTests.cs + Infrastructure/WebVitalsCollector.cs) injects PerformanceObservers 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 dated web-vitals-*.json artifact (wired into e2e.yml via WEB_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 (no MMCA.Common release); 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:740 Microsoft.Cache/redisEnterprise@2024-09-01-preview, database at :753, redis-connection-string secret injected at :771,849-850), so the shared cache / SignalR backplane substrate exists. The fan-out itself stays unexercised: Notification is still pinned maxReplicas: 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-freshness job in deploy.yml's needs fails the deploy when the latest successful monthly load-test.yml run 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 chromium e2e-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 pinned maxReplicas: 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:31 fails 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 projectsRESOLVED: Tests/WebAPI revived as MMCA.Common.API middleware unit tests; the orphaned combined MMCA.ADC.IntegrationTests was 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-NN ID with its blocker + resolution path + effort, and the recorded-not-scheduled choices have a Deliberate / accepted section. (Originally a separate TECHDEBT.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 reads Directory.Packages.props and fails the build if the thirteen MMCA.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 combined MMCA.ADC.Migrations.SqlServer archive csproj; the workspace ArchitecturalAnalysis.md outside 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,277 in both UI hosts use var(--mmca-primary), guarded by Tests/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): BrandColorTokenTests guards Primary only (Secondary has no drift test), and a few !important overrides + Store-specific cart CSS live in Common's shared app.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 chromium e2e-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 alertsMicrosoft.Consumption/budgets in main.bicep: a monthly RG budget (monthlyBudgetAmount, default $200) notifying the existing action group + alertEmailAddress at 80% actual and 100% forecasted spend. The automatic guard against an un-reverted surge silently billing for weeks.
  • Resource cost tagscommonTags (application / environment / component / managedBy / costCenter) stamped on every billable resource across main.bicep (App Insights, SQL server + all 5 DBs, Service Bus, Container App env, all 6 Container Apps) and foundation.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 every adc-* Container App is ≤ maxReplicas 2 and 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.ymlDONE 2026-06-30: added a workflow_call trigger to cost-guard.yml and a cost-guard job (uses: ./.github/workflows/cost-guard.yml, secrets: inherit) to deploy.yml's deploy.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: AddBrokerMessaging falls back to the in-process bus when no broker is configured, DataSourceResolver collapses the per-module logical sources onto one physical database (single context, FK constraints restored), and ModuleLoader boots 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), one ADC database via the resolver collapse (migrate the four DBs' data in), drop Service Bus/Redis/Gateway resources from main.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 no deploy.yml / main.parameters.json change 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-chain job.)
  • Enable lock files, add an SBOM step, add license scanning. → DONE (TD-01 closed, 2026-06-26): 58 committed packages.lock.json (RestorePackagesWithLockFile=true in Directory.Build.props:27; the Blazor WASM client + UI.Web host opt out via RestorePackagesWithLockFile=false — sidestepping the NETSDK1124 trimming-check that wedged the earlier bootstrap), and the supply-chain CI job's vuln-audit + SBOM are now blocking PR gates (deploy.yml:108-169, :146/:155 exit 1, in deploy.needs); license/deprecated reports stay advisory. Residual (now keeps two-axis impl at 9, not 10): the --locked-mode half is DONE (CI restore runs --locked-mode in 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_TOKEN bootstrapDONE: README.md is now a full getting-started guide (prerequisites incl. Docker, the GITHUB_TOKEN packages:read bootstrap with the local-source local.props alternative 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.IntegrationTests runs 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 in cross-service-tests.yml (a new job in the same workflow, so its result rides the existing cross-service-freshness deploy 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-clients Host() 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 in infra/DISASTER-RECOVERY.md. The weekly cost-guard.yml would 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 — AuthenticationService 7-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, in MMCA.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: BrandColorTokenTests guards the Primary token only (Secondary has no drift test), and a few !important overrides + Store-specific cart CSS live in MMCA.Common's shared app.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's e2e-gate invokes one browser leg instead of three (deploy.yml:488, rationale comment at :478-480); firefox/webkit cross-engine coverage moved to the nightly e2e.yml matrix, where continue-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 (a cross-browser-freshness gate) 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 a skip_freshness_gates workflow_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-root ArchitecturalAnalysis.md remains 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 at SpeakerSelfServiceTests.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 because PseudoLocalizationTests.cs:51 covers 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 the load-freshness gate 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:51 covers 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 quoted README.md:74 is 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 with load-freshness a recency-only check, deploy.yml:548, and Notification stays pinned maxReplicas: 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 in ACCESSIBILITY-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

  1. 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.)
  2. Rework the orphaned integration tier (#14) — per-service WAF tiers, ~345 tests, deploy-gated.
  3. Real erasure path + stop logging PII (#30) — IAnonymizable + anonymize-on-delete + export endpoint + log redaction. (Residual: cross-service export aggregation.)
  4. UnsavedChangesGuard sweep (#19 + #24) and admin-route authz (#25).
  5. 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-gate on 2026-07-02 (#28 closed, TD-06/07 done). (Residuals: the #18/#19 UI fitness gates and the #21 recorded SR pass.)
  6. 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.ScoreSessionAsync and the ScoreEventSessionsHandler persistence catch now filter when (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 AiScoreResponse sub-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; reasoning stays optional.
  • A-3 (doc-only) Speaker-overlap docs corrected: GetSpeakerSessionOverlapHandler, SpeakerSessionOverlapDTO/MultiSessionSpeaker docs, 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 with GetSessionSelectionDashboardHandler (!c.IsDeleted plus live-item count check), so a category whose only referenced item is soft-deleted is omitted entirely.
  • A-5 Duplicate guards added: Session.AddSessionCategoryItem and Speaker.AddSpeakerCategoryItem now reject a live duplicate association (codes Session.CategoryItem.Duplicate / Speaker.CategoryItem.Duplicate), mirroring AddSessionSpeaker; 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/ExportUserDataHandler compared the raw role claim string ordinally against UserRole.Organizer (via the implicit string conversion), denying organizers whose claim carried different casing. New case-insensitive UserRole.IsOrganizer(string?) helper used in both, with lowercase-claim regression tests.
  • A-7 (cosmetic): SessionLookupService dropped the misleading pageSize=10000 query param: the base /sessions endpoint 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); SpeakerDashboardService notes the same cap; the stale MMCA.ADC.slnx comment claiming the deleted combined MMCA.ADC.IntegrationTests project "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.