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

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 (thirtieth-cycle full re-score, 2026-09-04, pin v1.185.0, HEAD f831b8b8): Maturity 96.6% (313/324) / Implementation 85.1% (689/810), Σweight 81 with no N/A category; three score moves (§4 I8→9, §9 I9→8, §16 N/A→M2/I5), the last of which re-enters the denominators and is why both headline percentages fall while the 33 previously scored categories net +1 implementation point (maturity 309 unchanged, implementation 678→679). Ten categories were adversarially adjudicated (§4, §5, §7, §9, §12, §16, §23, §25, §27, §31): §4 was a confirmed lift, §9 and §16 were verifier corrections of the first pass, and the other seven were proposed lifts rejected as verified non-moves (§25 on both axes). The prior cycle (twenty-ninth, 2026-09-01, pin v1.179.0, HEAD 65bddd4b: 309/316 and 678/790 after the rubric v2 rebase of 2026-09-04, ADR-110; the 2026-09-01 figures were 313/320 and 685/800) moved §15 I7→8, §25 I8→7, §28 I8→9 and §33 M3→4 + I8→9. See the Index note for the cycle record. Tasks are ranked on both scorecard axes, one band per axis (two-axis policy adopted 2026-07-28):

  • Maturity band: every category scoring maturity < 4, ranked by priority = (4 − maturity) × weight.
  • Implementation band: every category scoring implementation <= 8, ranked by implPriority = max(0, 9 − implementation) × weight. The scheduling target stays 9, not 10, but the reason changed on 2026-08-01: a 10 is now awardable for an almost perfect implementation, so it is no longer unreachable. Ranking against 10 would instead put nearly every strong category in the band and drown the real gaps, so the 9→10 rung is recognition earned at re-score time, never scheduled work. 9 mirrors maturity's target of 4 for scheduling purposes.

Higher priority = bigger weighted gap = more index points per unit of effort. A category leaves each band independently and reaches the protect list only at maturity 4 AND implementation >= 9. The indices themselves are unchanged (still × 4 and × 10), so the trend line stays comparable across every prior cycle; the 9-target governs scheduling only.

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.185.0: Maturity 96.6% (313/324) / Implementation 85.1% (689/810), thirtieth cycle 2026-09-04). 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:553, 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. Verification pass 2026-07-23 (post-cycle, no score claims): stale claims corrected in place across this ledger and the scorecard: the #6 header's "2 of 4 services" inbox basis (all four services carry EnableInbox=true since the TD-02 close), #26's "pending manual Aspire verification + release" phrasing (shipped and deployed), the #29/#31 "activates on the next push" phrasing (gates live in deploy.needs since 2026-06-30), the scorecard's §8/§28 "re-quarantined" claim (E2E #5 was un-quarantined 2026-07-19, plain [Fact] at SpeakerSelfServiceTests.cs:58), lock-file count 58→65, resx pairs 40→53, MMCA.Common.* pin 1.117.0→1.123.0 in the §16/§32 rows, and drifted anchors (deploy.needs :783:791, load-freshness :548:553, coverage floor :83:210-212, sloWorkbook :278:425, Notification pin :1113:1424, CI.slnf:56:58, OrganizerConcurrencyTests.cs:26:27). The genuinely-open TD set today is TD-08, TD-15, TD-16 (older per-cycle "open TD-..." snapshots above are frozen provenance). The screen-reader-pass runbook lives centralized as adc-ACCESSIBILITY-SCREENREADER-PASS.md in Website docs-src/guides/ (2026-07-20 centralization); bare-name references below predate that move. Reconciled 2026-07-28 (twenty-fourth-cycle full re-score, pin v1.131.0, HEAD 2ec77796, one score down): §15 Best Practices & Code Quality implementation 8→7 (weight 2), the only score move and the only rank change on either axis; maturity holds 4, independently re-derived, so #15 stays in the protect set while taking the top row of the implementation band at implPriority 4. The basis is hygiene drift, not a code-quality regression: an audit suppression expired by its own written removal condition (Directory.Build.props:49-51 vs its comment at :41-48), three undated global NoWarn codes (:22), and the MAUI MMCA.ADC.UI project sitting outside every CI build and outside the CI-audited dependency graph (MMCA.ADC.CI.slnf:25, deploy.yml:288), which is precisely the graph the :8-12 suppressions exist for. Band totals move to 15 categories / 35 gap points (count unchanged, §15 was already in the band) and 95.1% of the 90% attainable ceiling. No closures: closure needs maturity 4 AND implementation >= 9 independently, and all four maturity-band items are still M3 and all four still I8, with none of the 15 implementation-band categories reaching 9, so nothing moves to the protect list and the maturity band is byte-identical (#21 at 3, #12/#22/#33 at 2, 4 categories / 9 points). Re-verified still-open levers: #21's screen-reader results log is still the empty _yyyy-mm-dd_ placeholder (adc-ACCESSIBILITY-SCREENREADER-PASS.md:60-62), #22 is still chromium-only gating (deploy.yml:505) with firefox/webkit advisory on the Mon/Thu schedule (e2e.yml:131, cron :43), #12 is still scale-pinned (infra/main.bicep:1447) with a monthly out-of-band capacity proof (load-test.yml:18). Adjudicated DEFERRED, not open: the #27 impl 8→9 pseudo-loc candidacy, rejected for a third time (21st, 22nd, 24th) on byte-identical evidence, is now recorded in Deliberate / accepted with its cost and explicit re-open triggers rather than carried as a live candidacy to re-reject a fourth time. Also re-rejected and recorded so they are not re-proposed: #24 impl 8→9 and #33 M3→4 / I8→9; #6 and #30 sit at I9, already at the scheduling target, so their recorded "9→10" candidacies are out of scope for both bands. New: TD-17 under #33 (the Service Bus emulator parity tier is now dispatch-only after hanging to its 8-minute timeout on 7 of 7 runs, so #33's header basis is corrected from "nightly plus recency gate" to "no schedule, no gate") and TD-18 under #15 (the MAUI CI-enforcement gap, recorded rather than fixed because a MAUI CI build cuts against the 2026-07-18 Actions-minute reduction). TD-16 refreshed, worse: HappeningNow.razor.cs is now exactly 400 lines against the enforced cap, and the recorded 360-379 band was stale (SpeakerDetail.razor.cs is 386, not 365); current measured set 400/386/379/376/367/365/362 with two recorded paths corrected. TD-15 was NOT re-verified this run and is left exactly as written; its figures are not restated as re-confirmed. Evidence refresh (no score move): the arch-test suite is now 29 test classes / 31 .cs files / 91 executed methods, re-run green 2026-07-28 (91/91), up from 26/28/82, and 90 of the 91 are inherited from the shared rule library after the §13 alert-runbook pairing gate was lifted upstream (ObservabilityConventionTests.cs:7 is now a bare thin subclass), leaving the TD-14 Profile-form guard (FormsConventionTests.cs:31) as the single ADC-local method; ADR set 001-060. Anchors refreshed repo-wide: deploy.yml e2e-gate :488→job at :500 with browsers: '["chromium"]' at :505 and rationale :478-480:493-499, deploy.needs :791:829, the freshness jobs re-split (cost-guard :488, dr-freshness :513, load-freshness :570, cross-service-freshness :627) with their skip checks at :526/:583/:642, e2e.yml:119:131, infra/main.bicep:1424:1447, load-test.yml:8:18, cross-service-tests.yml emulator job at :142 with its dispatch-only condition at :144. The genuinely-open TD set today is TD-08, TD-15, TD-16, TD-17, TD-18. Reconciled 2026-08-01 (twenty-fifth-cycle full re-score, pin v1.135.0, HEAD 995a7886, no moves): every category re-confirmed at its prior score from evidence read this run, so there are no closures, no new items and no re-ranks: both bands are byte-identical (maturity 4 categories / 9 points, #21 at 3 and #12/#22/#33 at 2; implementation 15 categories / 35 points). Closure needs maturity 4 AND implementation >= 9 independently, and all four maturity-band items are still M3/I8 while none of the 15 implementation-band categories reached 9. All six adversarial adjudications this cycle were proposed implementation lifts and all six were rejected: §5 8→9 (DTOs live in the Shared assembly with horizontal mapper/validation/specification folders, the layered-by-project hybrid is unchanged, and AdcArchitectureMap.cs:12-44 omits MMCA.ADC.Notification.Application from the enforced set), §13 9→10 (three ENABLED production alerts have no runbook triage section and sit outside the pairing gate's scope, including the sev-1 gateway-availability alert at infra/main.bicep:481), §24 8→9 (the named bUnit lever shipped, but client validation does not mirror the server's cross-field and format rules and the error summary covers 7 of 15 MudForm forms: both are now recorded as §24's levers, replacing "not yet identified"), §27 8→9 (fourth rejection, byte-identical evidence plus one new culture-formatting violation), §31 8→9 (the surge/revert automation is not pulled), and §33 8→9 (second rejection: see the rewritten TD-17 below). TD-17 is HALF CLOSED and its blocker text was invalid: the servicebus-emulator-smoke job is back on the weekday nightly since 2026-07-29 (cross-service-tests.yml:144-146 needs: should-run + if: needs.should-run.outputs.run == 'true' under cron: '0 6 * * 1-5' at :26,:30, timeout-minutes: 10 at :148), and the recorded root cause was wrong: the comment at :130-143 records per-test bus re-provisioning against an admin plane throttled at roughly 1 op/sec (IAsyncLifetime plus xUnit per-Fact class instantiation), fixed by hoisting the bus to the collection fixture and wall-clock bounding both startup phases, not the companion SQL image. The remaining half is open: the tier is continue-on-error: true (:149) and gates nothing, since cross-service-freshness keys off the cross-service job (:124-128, gate at deploy.yml:663). TD-16 re-measured, and the headline is no longer true: HappeningNow.razor.cs is 394, not 400, and the high-water mark moved to SessionSelectionDashboard.razor.cs at 395, so the "flush at the cap, zero headroom" framing is retired in favour of 5 lines of headroom; current measured set 395/394/386/376/367/365/362 at HEAD 995a7886, seven files within 38 lines of the 400 cap, still effort S. TD-08 and TD-18 re-confirmed open (TD-18's gating-scan anchor drifted deploy.yml:288:319). TD-15 was NOT re-verified for a second consecutive cycle and is left exactly as written; its cost figures are not restated as re-confirmed. Anchors refreshed repo-wide: deploy.yml e2e-gate :500:531 with browsers :505:541, deploy.needs :829:866, the freshness jobs re-split again (cost-guard :519, dr-freshness :549, load-freshness :606, cross-service-freshness :663) with their skip checks at :562/:619/:678, coverage floor :210-212:254, the gating vuln scan :288:319, --locked-mode restores at :199/:299, e2e.yml:131:144 with the nightly matrix replaced by alternating single-engine crons at :49,:50, infra/main.bicep:1447:1530 (and the SLO/budget/SQL anchors re-derived), Directory.Build.props suppression :49-51:54 and NoWarn :22:26, ADC pin :123:139 at 1.135.0, lock files 65→66, ADR set 001-064. Reconciled 2026-08-14 (twenty-sixth-cycle full re-score, pin v1.152.0, HEAD 19021d93, no moves): every category was re-confirmed at its prior score from evidence read this run, so again there are no closures, no new items and no re-ranks: both bands are byte-identical (maturity 4 categories / 9 points, §21 at 3 and §12/§22/§33 at 2; implementation 15 categories / 35 points). All eight adversarial adjudications were proposed lifts and all eight were rejected: §5 8→9 (DTOs and their mappers still outside the slice, and AdcArchitectureMap.cs:12-43 still has no Module("Notification", ...) entry, now named as TD-19), §7 8→9 (the bidirectional sync-gRPC red flag broadened to a second pair, Identity-Notification), §12 M3→4 (zero commits touched load-test.yml, deploy.yml or Tests/Load/ since the prior HEAD), §13 9→10 (three of the six ENABLED production alerts still have no runbook, including the sev-1 gateway-availability alert, whose anchor moves infra/main.bicep:481:496-502, severity: 1 at :502; §13 sits at I9, outside both bands), §15 7→8 (all three downgrade grounds intact, and the expired suppression is further past its removal condition now that the pin is v1.152.0), §23 8→9 (WASM code-split and image optimization both still open), §28 8→9 (the new state-management bUnit coverage is a within-band improvement) and §31 8→9 (the surge/revert automation is still not pulled, cost-guard.yml:4,:12,:17,:59,:83). New: TD-19 under §5 (the Notification module is absent from the enforced architecture map, effort S), which replaces §5's "lever not yet identified" band row. TD-16 re-measured, and the headroom narrowed: the high-water code-behind rose 395→398 of the 400 cap, leaving 2 lines rather than 5. TD-17 unchanged in substance, anchors corrected: job :145, needs :146, if :147, timeout-minutes :149, continue-on-error :150, schedule workflow_dispatch :26 + cron '0 6 * * 1-5' :31 (the recorded :26,:30 was wrong), gate-keying comment :126-129. TD-15 was NOT re-verified for a third consecutive cycle and is left exactly as written. Evidence refresh (no score move): axe coverage is 31 test methods over roughly 29 distinct pages (AccessibilityTests.cs:21-365), not 17 pages; the routable-page denominator is 49 @page files under Source (48 excluding the MAUI-only DeviceSettings.razor), not 37, so §27's deferred lift now costs roughly 45 pages; the Notification scale pin moves infra/main.bicep:1530:1616; §24's error-summary ratio is 8 of 18 MudForm-bearing pages (19 forms), not 7 of 15; TD-18's MAUI NoWarn CA5392 anchor moves MMCA.ADC.UI.csproj:131:143. Indices hold Maturity 97.2% (311/320) / Implementation 85.6% (685/800), ADR set 001-078. Reconciled 2026-08-23 (twenty-seventh-cycle full re-score, pin v1.160.0, HEAD 96f0919a, two scores down): §4 Domain-Driven Design implementation 9→8 (weight 3; public-setter cross-aggregate navigations on Session/Sponsor/Activity, aggregate-external validation of Event's newer optional fields against the repo's own Sponsor convention, and Event.OrganizerContactEmail as a raw string where the Email VO covers the same concept on User/Speaker) and §22 Responsive & Cross-Browser implementation 8→7 (weight 2; the rubric's density-options criterion has zero adoption and content reflow is only partial on the 17 non-DataGrid table pages), so the implementation band grows to 16 categories / 40 gap points: §4 enters the band for the first time (implPriority 3, maturity 4 holds, so #4 stays in the protect set) and §22 rises to the joint top at implPriority 4 alongside §15. No closures (all four maturity-band items still M3 with their levers re-verified open: the SR-pass log still the empty placeholder at adc-ACCESSIBILITY-SCREENREADER-PASS.md:62, #12 still scale-pinned at infra/main.bicep:1648 with its rationale at :1643-1647, #22 still chromium-only at deploy.yml:541, #33's parity tier still advisory at cross-service-tests.yml:150), and the maturity band is byte-identical for a fourth consecutive cycle (4 categories / 9 points). All eight adversarial adjudications were proposed lifts and all eight were rejected (§5, §7, §15, §17 as a 9→10, §18, §21 as an M3→4 + I8→9 pair, §28, §31). New: TD-20 under #28 (the deploy-gating chromium E2E/axe/CWV suite is CONDITIONAL: e2e-gate runs only when the changes job marks the diff UI-affecting, deploy.yml:538 with rationale :533-537, and the deploy job accepts a skipped gate, :896 with comment :880-883, so a backend-only, infra-only or script-only merge deploys with no browser, axe or CWV run; a matching amendment is recorded in Deliberate / accepted), which also names §28's previously unidentified band lever. Wording corrected ledger-wide: the integration-tests job is PR-only (if: github.event_name == 'pull_request', deploy.yml:389) and is NOT in deploy.needs (:866), so the "gates every deploy" / "deploy-gating MMCA.ADC.Integration.slnf" phrasing under #30/#14/#11/#8/#9 is rewritten to "gates every PR (required check on an up-to-date branch)"; TD-03's closure itself stands. TD-16 re-measured, unchanged at the top but wider: high-water 398/394/386 identical to 2026-08-14, but the within-38-lines set grew from seven to eight files (three grew: PublicSessionList.razor.cs 367→398, ADCHome.razor.cs 341→380, EventDetail.razor.cs 365→377), so TWO files now sit at 398. TD-17/TD-18/TD-19 re-confirmed open on current anchors; TD-15 NOT re-verified for a fourth consecutive cycle (no billing read; figures stand as written). Provenance: the §15 band row's "pins v1.135.0 at Directory.Packages.props:139" is doubly stale, now v1.160.0 at Directory.Packages.props:92-110, twenty-five releases past the v1.121.0 SQLite sweep. Indices move to Maturity 97.2% (311/320) / Implementation 85.0% (680/800), ADR set 001-096. Reconciled 2026-08-31 (twenty-eighth-cycle full re-score, pin v1.175.0, HEAD b04b3a3e, no moves): every category was re-confirmed at its prior score from evidence read this run, so there are no closures, no new items and no re-ranks: both bands are byte-identical (maturity 4 categories / 9 points, #21 at 3 and #12/#22/#33 at 2; implementation 16 categories / 40 points). Closure needs maturity 4 AND implementation >= 9 independently: all four maturity-band items were re-verified OPEN (#21's screen-reader results log still the empty placeholder row at adc-ACCESSIBILITY-SCREENREADER-PASS.md:62; #12 still pinned maxReplicas: 1 at infra/main.bicep:1591 with the right-sizing rationale ending :1589, the recorded :1648 having drifted back; #22 still chromium-only at deploy.yml:638; #33's parity tier still continue-on-error: true at cross-service-tests.yml:150), and all 16 implementation-band categories remain at implementation <= 8. All ten adversarial adjudications were proposed lifts and all ten were rejected (§5, §7, §12 with a fresh negative: PR #161 withdrew the Conference UI IUiReadCache opt-in after main-branch e2e-gate staleness failures; §13 as a 9→10, third rejection; §15, with the expired-suppression ground strengthened at pin v1.175.0; §21 as an M3→4; §23, the two v1.175.0 framework capabilities being inert in ADC; §24; §27, a fifth rejection on a denominator that grew 49→53 routable pages; §31 as an 8→10). TD-16 re-measured, and the flush-at-cap state is back on a file the ledger never named: the high-water is now SessionDetail.razor.cs at exactly 400 of the 400 cap (zero headroom; cap confirmed unoverridden at UIArchitectureConventionTestsBase.cs:22 with no subclass override in UIArchitectureConventionTests.cs:10), and the within-38-lines set grew to NINE files: SessionDetail 400, EventDetail 399, HappeningNow 396, SpeakerDetail 396, SessionSelectionDashboard 395, PublicSessionList 388, ADCHome 373, ConferenceCategoryDetail 373, SessionLive 370 (neither recorded 398 file is at 398 any more). TD-17/TD-18/TD-19/TD-20 re-confirmed open with anchors corrected (TD-17's paired gate deploy.yml:663:760; TD-18's CI.slnf-scoped vulnerable scan deploy.yml:319/:328:416/:425; TD-19's :12-43 map anchor still accurate; TD-20's full anchor set :538/:533-537/:896/:880-883:635/:630-634/:1022/:1006-1009 with the job at :628 and deploy.needs at :992). TD-15 was NOT re-verified for a fifth consecutive cycle and stands as written. Measured figures refreshed: aria attributes 64/18→100 across 42 .razor files, resx pairs 53+53→68+68, §24 error summaries →9 of 21 MudForm instances (20 files), lock files 66→67, routable @page denominator 49→53, the §7 sync graph re-measured at seven gRPC client registrations / two bidirectional cycles / seven protos. Indices hold Maturity 97.2% (311/320) / Implementation 85.0% (680/800), ADR set 001-104, pin v1.175.0 at Directory.Packages.props:100-123 (16 lockstep packages). Reconciled 2026-09-01 (twenty-ninth-cycle full re-score, pin v1.179.0, HEAD 65bddd4b, four score moves): #33 is CLOSED on both axes (scorecard §33 maturity 3→4 and implementation 8→9): servicebus-emulator-smoke carries no continue-on-error (cross-service-tests.yml:153) under an "AUTHORITATIVE SINCE 2026-08-31 (TD-17)" header that forbids re-adding one (:126-137, :135), cross-service-freshness now requires both broker jobs to have concluded success (deploy.yml:874, deploy fails otherwise :885, in deploy.needs :1054, required :1089), and ADC_BROKER=servicebus gives the inner loop a real Service Bus emulator profile (AppHost/Program.cs:91,:93-94, opt-in; the default stays RabbitMQ at :86), so TD-17 is ticked. #28 leaves the implementation band (scorecard §28 8→9) on a markup-snapshot regression tier in all three bUnit projects (ComponentsSnapshotTests.cs:27, assertions :68,:86,:123,:142, 12 [Fact]s over 12 committed .html baselines, missing baseline reported as a non-match, all three projects in MMCA.ADC.CI.slnf:43,:49,:55) plus the new backend-test-gate (deploy.yml:394-396, :1054, :1093). #15 rises to implementation 8 with both effort-S hygiene levers struck: Directory.Build.props now carries zero NuGetAuditSuppress items and no GHSA id (the expired SQLite entry and the System.Private.Uri MAUI entries are gone; the live high advisory is remediated by a patched SSH.NET 2026.0.0 pin at Directory.Packages.props:83), and the global NoWarn line is CS1591;EXTEXP0001;S8970 at :31 with each code dated and justified (:16-21, :22-26, :27-30) and RMG020 scoped to .Application projects (:57, rationale :50-55). #25 falls to implementation 7 and joins the band (scorecard §25 8→7, maturity 4 held, the proposed 4→3 rejected): adc-NavigationFlow.md (557 lines) has zero occurrences of /activities, /engage or speaker/qr, leaving 7 of the 53 routable @page files undocumented and the authorization enumeration at :532-534 incomplete, with two nav items undescribed (ConferenceUIModule.cs:29,39) and no ADC-side navigation contract test; its new lever is documenting the seven routes plus the two nav items (effort S), with an ADC-side drift gate modeled on Common's NavigationContractTests as an optional second (effort M). TD-19 is CLOSED (AdcArchitectureMap.cs:51-54 now registers Notification, so the enforced map covers all four modules) and TD-17 is CLOSED as above. TD-16 re-measured and the picture inverted: the high-water code-behind is PublicSessionDetail.razor.cs at 386 of the 400 cap (14 lines of headroom), then SpeakerDetail 346, PublicSessionList 336, SessionLive 333, SessionFeedback 325, SessionLiveModerationPanel 316, SpeakerDashboard 314, ADCHome 313, SessionDetail 309, so exactly one file is within 38 lines of the cap (was nine) and the flush-at-cap state is retired; the cap is unchanged at 400 and unoverridden (UIArchitectureConventionTests.cs:10-12), and TD-16 stays open because §18 holds at implementation 8. TD-20 is PARTIAL, not ticked: the closed half is the new backend-test-gate (deploy.yml:394-396, in deploy.needs :1054, required :1093), which carries the exact complement of e2e-gate's condition so no code deploy runs with zero test execution; the open half is unchanged, e2e-gate is still ui-scoped (:688, job :677) and deploy still accepts a skipped gate (:1092), so a backend-only deploy still reaches production with no browser run. TD-18 re-confirmed open with anchors refreshed: MAUI still absent from MMCA.ADC.CI.slnf (:25-26), NoWarn CA5392 still ungated (MMCA.ADC.UI.csproj:151), the vulnerable-package scan still CI.slnf-scoped (deploy.yml:465), and the new maui-audit.yml (weekly cron plus dispatch :36, android-only :19, header claiming only the supply-chain half :14) has never run. TD-21 is NEW (under #12, effort S, OPEN): the 2026-09-01 scheduled k6 run failed 96.06% http_req_failed against the gateway per-client-IP edge limiter, and the 35-day load-freshness recency gate blocks every deploy from 2026-09-05. TD-15 was NOT re-verified for a seventh consecutive cycle (still unread at the 2026-09-04 re-score) and stands as written; TD-08 was likewise not re-verified this cycle. Nine adversarial adjudications (§4, §5, §7, §12, §21, §22, §24, §25, §27): eight proposed lifts rejected (§4 keeps two of its three grounds after the public setters were fixed in #152; §5 keeps the layered-by-project hybrid cap even with TD-19 closed; §7 byte-identical at seven gRPC registrations and two bidirectional cycles; §12 pulled its fan-out lever but opened TD-21; §21's screen-reader row still the empty placeholder at adc-ACCESSIBILITY-SCREENREADER-PASS.md:62; §22 still chromium-only at deploy.yml:691; §24 closed lever (b) at 21/21 error summaries but keeps lever (a); §27 a sixth rejection, still DEFERRED) and §25 confirmed down. Both bands re-derived: maturity 3 categories / 7 points (#21 at 3, #12 and #22 at 2); implementation 14 categories / 35 points. Measured figures refreshed: §24 error summaries 9/21→21 of 21, routable @page files 53, cross-service integration tests 9→10 (the new TwoReplicaHubFanOutTests.cs:49 cross-replica SignalR proof, green in nightly run 33500459363), Notification maxReplicas 1→2 (infra/main.bicep:1596, rationale :1586-1595, and no maxReplicas: 1 remains anywhere in the file). Indices move to Maturity 97.8% (313/320) / Implementation 85.6% (685/800), ADR set 001-106, pin v1.179.0 at Directory.Packages.props:105-128 (16 lockstep packages). The genuinely-open TD set today is TD-08, TD-15, TD-16, TD-18, TD-20 (partial) and TD-21; TD-17 and TD-19 both closed on 2026-09-01.

Scope: 4 categories sit below maturity 4 (§12/§16/§21/§22; §16 entered on 2026-09-04 at maturity 2, the first new maturity-band entrant since §22 reopened on 2026-07-21, because the AI session-scoring feature makes the rubric v2 category scoreable; §22 was REOPENED on 2026-07-21 after the 2026-07-18 CI-minute reduction cut the deploy e2e-gate to chromium only, deploy.yml:691, leaving firefox/webkit nightly-advisory, e2e.yml:144, and thinner still since the 2026-07-29 move to alternating single-engine legs, e2e.yml:49,:50; #33 closed 2026-09-01 on TD-17); 30 categories score maturity 4 (protect, don't regress); none are N/A. On the implementation axis, 14 categories score implementation <= 8** and are ranked in their own band below (40 gap points; §16 entered at 5 and §9 fell to 8 on 2026-09-04, while §4 rose to 9 and left); **20 categories sit at maturity 4 AND implementation >= 9, which is the only combination that reaches the protect list.

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 localStorage IMPLEMENTED and RELEASED (cookie-only refresh, live in prod; the stale "pending manual Aspire verification + release" phrasing was removed on the 2026-07-23 verification pass): 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 string RESOLVED (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 headers RESOLVED (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. Shipped and deployed (stale "pending manual Aspire verification + release" note removed 2026-07-23). 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. Qualified 2026-08-23 (TD-20): the gate is conditional since 2026-07-29: e2e-gate runs only when the diff is UI-affecting (deploy.yml:688, job :677) and deploy accepts a skipped gate (:1092), so a backend-only merge deploys with no browser run. Re-scored 2026-09-01: scorecard §28 implementation 8→9, so #28 leaves the implementation band on two things landed in PR #162: a markup-snapshot regression tier in all three bUnit projects (ComponentsSnapshotTests.cs:27, assertions :68,:86,:123,:142, 12 [Fact]s over 12 committed .html baselines, a missing baseline reported as a non-match, all three projects in MMCA.ADC.CI.slnf:43,:49,:55), and the new backend-test-gate (deploy.yml:394-396, in deploy.needs :1054, required :1093), which carries the exact complement of e2e-gate's condition. TD-20 is therefore PARTIAL, not closed: the "no code deploy runs with zero tests" half is shut, but the browser half is unchanged (e2e-gate still ui-scoped at :688, skipped gate still accepted at :1092)

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 forms: EventFeedbackTests (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 bUnit: DataGridListPageBase 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 test, ManagementRouteAuthorizationTests (reflection over Conference.UI): admin-namespace pages must keep [Authorize(Roles="Organizer")], the set is asserted non-empty (no vacuous pass), and public pages must stay anonymous at the page level. Closes the #25 residual.
  • Wire axe-core (Deque.AxeCore.Playwright) + ≥1 a11y assertion (TD-06) → DONE (2026-07-02, ticked on the 2026-07-03 reconciliation): the axe-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 gate every UI-affecting deploy (conditionality recorded 2026-08-23 as TD-20: a skipped gate does not block a non-UI merge, deploy.yml:538,:896).
  • 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. → markup-snapshot half DONE 2026-09-01 (PR #162): deterministic golden-markup baselines in all three bUnit projects (ComponentsSnapshotTests.cs:27, 12 [Fact]s / 12 committed .html baselines, OS-independent, missing baseline = non-match), gating every PR via MMCA.ADC.CI.slnf:43,:49,:55. Pixel-level Playwright visual regression is still open, and 12 components is a small slice of the surface: both are part of §28's remaining 9→10 rung.

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 (live in deploy.needs, deploy.yml:791, since 2026-06-30)

Strong in-app resilience (Polly, SQL retry, outbox, health probes), now with a first-class recovery story. (Flags severity-adjusted low for scale, but collectively they drive the score.) Status (2026-06-27): RTO/RPO note, LTR + executed restore drill, SLO alerts/workbook, and the fault-injection + graceful-shutdown tests are all done, the graceful-shutdown half was CI-verified (GracefulShutdownTests), which lifted scorecard §29 impl 8→9. Correction (2026-06-29 re-score): the v1.86.0 claim that the scheduled DR-drill closed the maturity-4 lever was reversed. dr-drill.yml:27-29 is a weekly cron that gates nothing (absent from deploy.yml:284's needs; CLAUDE.md:251 buckets it among the non-deploying operational workflows), so it is Consistent/M3, not an automatic CI gate (the same standard §28 is held to). Scorecard §29 is maturity 3 / impl 9. The open maturity-4 lever is to make the recovery proof actually block a merge/deploy; the conference-day minReplicas:2 choice stays a deliberate accepted-risk deferral.

  • Undefined RTO/RPO anywhere in repo/infra/docs.
  • Untested DB restore; Basic-tier 7-day PITR default, no LTR. deploy.yml:258-289, infra/main.bicep:207-222.
  • SPOFs without documented risk acceptance: one SQL server (publicNetworkAccess Enabled), one Container App Environment, all apps minReplicas:1. infra/main.bicep:149-159,270,….
  • No failure/chaos testing; graceful shutdown unverified. RESOLVED: fault-injection (Common) + Gateway graceful-shutdown test (see fix item below).
  • No reliability targets/alerting: App Insights wired but no metric alerts/action groups. infra/main.bicep:127-147.

Fix (right-sized for the real load)

  • Write down RTO/RPO + a single-region risk-acceptance note: infra/DISASTER-RECOVERY.md (targets table, accepted SPOFs, backup posture, recovery runbook).
  • Enable LTR/geo-redundant backups and run one restore drill: LTR (P4W/P12M/P1Y) added on all four live ADC_* DBs (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. Live in deploy.needs since 2026-06-30 (phrasing refreshed 2026-07-23). 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

[ ] #16 · AI-Native Application Architecture · 2 → 4 (weight 2, priority (4-2)×2=4) · OPENED 2026-09-04 (thirtieth-cycle re-score): scorecard §16 scored for the first time at maturity 2 / impl 5. Rubric v2 (ADR-110) declared the category N/A on the ground that no product feature calls a model, but ADC's organizer-facing AI session scoring has called the Anthropic Messages API since 2026-04-04 (commit 5c082663; structured outputs and the computed overall added by ADC #176 on 2026-09-03) and runs in production (ANTHROPIC_API_KEY from deploy.yml:1122, Key Vault secret injected as Anthropic__ApiKey via managed identity, infra/main.bicep:1007,:1315), so the rubric's own applicability rule ("score it as soon as a single feature does", ArchitectureEvaluationCriteria.md:473) applies and weight 2 re-enters both denominators. This is the top item on BOTH bands: priority 4 here and implPriority 8 in the implementation band below.

  • What is already right (do not re-do): the model call sits behind an Application-layer port (Source/Modules/Conference/MMCA.ADC.Conference.Application/Sessions/UseCases/DecisionSupport/ScoreEventSessions/IAiScoringService.cs:6) with the Anthropic adapter, records and prompt confined to Infrastructure (Source/Modules/Conference/MMCA.ADC.Conference.Infrastructure/Sessions/Scoring/AnthropicScoringService.cs:16, DI at DependencyInjection.cs:33 with the anthropic-version header pinned); output is schema-constrained (additionalProperties:false, required list, AnthropicScoringService.cs:255) and partial or refused responses are rejected (:119); the trigger is permission-gated and human-initiated (SessionSelectionController.cs:29); the recovery sweep cannot start unrequested paid work (SessionScoringSweepJob.cs:168). No agent, tools or retrieval store exist, so those two rubric criteria do not apply.
  • TD-22 (recorded 2026-09-04, under #16, effort M) · no evaluation suite of any kind gates a prompt or model change. The 21 adapter tests are parse/failure contract tests against a FakeAnthropicHandler (Tests/Modules/Conference/MMCA.ADC.Conference.Infrastructure.Tests/Services/AnthropicScoringServiceTests.cs:39) and the integration tier substitutes FakeAiScoringService; no golden cases, no judge or rubric scoring, no regression threshold, no workflow job. This is the rubric red flag "a prompt or model change shipped with no evaluation run", fully open, and it is the maturity 2→4 lever (an enforced evaluation gate is the "enforced by CI" dividing line). Blocker: a real-model evaluation costs tokens per run and needs the key in CI; a recorded-response (golden transcript) suite avoids both for the regression half. Resolution path: a small golden set of sessions with expected score bands, replayed through the real prompt assembly against recorded responses on every PR, plus an opt-in live judge run on prompt or model changes; wire it as a job that deploy needs. Effort: M.
  • TD-23 (recorded 2026-09-04, under #16, effort S-M) · untrusted text reaches the prompt unguarded, the prompt is unversioned, and cost is a log line. Externally submitted session title/description and speaker tagline/bio are interpolated straight into the user prompt with no delimiting, injection handling or PII redaction (AnthropicScoringService.cs:187) and the system prompt carries no anti-injection instruction (:160); the prompt is a source-controlled const with no version identifier and the model id is a hardcoded literal (:22) while persisted scores record only ModelId (ScoreEventSessionsHandler.cs:83), so a prompt edit silently changes score semantics; token usage is logged per session (:269) and the only OTel instrument is a terminal-failure counter (SessionScoringProcessor.cs:96), with no cost metric and no runaway-spend alert. Blocker: none, scheduled work. Resolution path: wrap the untrusted fields in explicit delimiters with an instruction to treat them as data, strip obvious PII before the call, add a PromptVersion constant persisted next to ModelId, and emit input/output token counters tagged by model and prompt version with a monthly-spend alert in infra/. Effort: S-M. Closing both red flags lifts implementation 5→7.
  • Record the feature in an ADR (or amend ADR-061, which today only lists the key): model choice, prompt change protocol, evaluation expectations and the cost ceiling. Effort S. Not a scoring lever on its own, but the maturity axis measures governance and today there is none that is AI-specific. The ADR-110 "N/A in all three repos" wording is a separate Website-side correction for /update-adrs.

[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 extension point (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 path: IAnonymizable + anonymize-on-delete (immediate erasure). (A scheduled-purge backstop for rows soft-deleted by other paths is optional now that delete erases inline.)
  • Redact/tokenize PII before logging: done in UserRegisteredHandler.
  • Add an export/access endpoint: GET /users/{userId}/export (Identity-owned data); cross-service bookmark/notification aggregation is the remaining 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 boundary 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, gating every PR as a required check; wording corrected 2026-08-23, the integration-tests job is PR-only, deploy.yml:389, not in deploy.needs): (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.
    • Adjudicated again 2026-07-28 (drift wave): DEFERRED, with the cost stated. The lever cannot be closed cheaply. Its load-bearing assertion is no-horizontal-overflow under the pseudo pass's ~40% text expansion, which is a RENDERED-LAYOUT property: bUnit has no layout engine, so a non-browser tier over the full route table cannot close it at any route count. The only approach that moves the score is more browser cases on the deploy-gating chromium e2e-gate, and every case added there is paid on every future PR: the same recurring-minute pressure that produced the §22 regression when the gate was cut to chromium-only on 2026-07-18. Deferred deliberately rather than part-done; §27 holds M4/I8. Revisit if the e2e budget changes.

🟡 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 PR (required check on an up-to-date branch; wording corrected 2026-08-23: the integration-tests job is PR-only, if: github.event_name == 'pull_request' at deploy.yml:389, and protects production through branch protection, not deploy.needs). 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:210-212). 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; PR-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 PR (required check; wording corrected 2026-08-23).
  • (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 create RESOLVED: 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 axe scans went green on validation run 28604877733 (impl 7→8; coverage re-counted 2026-08-14 at 31 axe test methods over roughly 29 distinct pages, AccessibilityTests.cs:21-365, including the conference-day surfaces, so the recorded 17-page figure is retired). 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; 31 axe test methods over roughly 29 distinct pages as re-counted 2026-08-14, :21-365); 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; conditional since 2026-07-29 per TD-20, so a non-UI merge deploys without an axe run. Coverage note 2026-08-23: four routable pages shipped 2026-08-19 with no axe coverage yet.)
  • (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). (Conditional since 2026-07-29, TD-20: runs only on UI-affecting diffs.)

[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): nine code-behinds sit within 38 lines of the convention ceiling, the MaxCodeBehindLines => 400 cap (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/UIArchitectureConventionTestsBase.cs:22), so a method added to any of them fails the gate rather than being caught in review. Re-measured 2026-08-14 (twenty-sixth cycle) at HEAD 19021d93, and the headroom is narrowing again. The high-water mark is Source/Modules/Conference/MMCA.ADC.Conference.UI/Pages/SessionSelection/SessionSelectionDashboard.razor.cs at 398, up from 395, so headroom against the 400 cap fell from 5 lines to 2; SessionDetail.razor.cs also rose 376→382. Current measured set: SessionSelectionDashboard 398, HappeningNow 394, SpeakerDetail 386, SessionDetail 382, PublicSessionList 367 (Pages/Public/), EventDetail 365, SessionLive 362 (the twenty-fifth cycle read 395/394/386/376/367/365/362 at HEAD 995a7886; the "flush at the cap, zero headroom" framing stays retired). Re-measured 2026-08-23 (twenty-seventh cycle) at HEAD 96f0919a: the top is unchanged (SessionSelectionDashboard 398 / HappeningNow 394 / SpeakerDetail 386, still 2 lines of headroom) but the band WIDENED from seven to eight files, three of which grew since 2026-08-14: PublicSessionList.razor.cs 367→398 (a second file at 398), ADCHome.razor.cs 341→380, and EventDetail.razor.cs 365→377. Re-measured 2026-08-31 (twenty-eighth cycle) at HEAD b04b3a3e: the flush-at-cap state is BACK, on a file this ledger had never named. The high-water is now Source/Modules/Conference/MMCA.ADC.Conference.UI/Pages/Session/SessionDetail.razor.cs at exactly 400 of the 400 cap, zero headroom (the cap confirmed unoverridden: UIArchitectureConventionTestsBase.cs:22 MaxCodeBehindLines => 400, no override in the sealed subclass UIArchitectureConventionTests.cs:10), and the band WIDENED again from eight to NINE files: SessionDetail 400, EventDetail 399, HappeningNow 396, SpeakerDetail 396, SessionSelectionDashboard 395, PublicSessionList 388, ADCHome 373, ConferenceCategoryDetail 373, SessionLive 370. Neither of the two files recorded at 398 is at 398 any more, so the recorded measurement understated the pressure. Re-measured 2026-09-01 (twenty-ninth cycle) at HEAD 65bddd4b, and the picture inverted: the flush-at-cap state is RETIRED and the band collapsed from nine files to one. The high-water mark is now Pages/Public/PublicSessionDetail.razor.cs at 386, 14 lines of headroom, and the rest of the measured set has fallen well clear: SpeakerDetail 346, PublicSessionList 336, SessionLive 333, SessionFeedback 325, SessionLiveModerationPanel 316, SpeakerDashboard 314, ADCHome 313, SessionDetail 309 (from 400). Exactly one file is within 38 lines of the cap (was nine); the cap itself is unchanged at 400 and still unoverridden (UIArchitectureConventionTests.cs:10-12). The item stays open because scorecard §18 holds at implementation 8 and the extraction is applied file by file rather than systematically, but the "one edit away from a red gate" urgency is gone. Blocker: none, this is scheduled work. Resolution path: presentational sub-component extraction per the TD-13 pattern above, markup moved verbatim so the rendered DOM and the E2E selectors are unchanged. Effort: S. This is what took scorecard §18 implementation from 9 to 8 in the twenty-second cycle; maturity holds 4 on the gate.

[x] #8 · Data Architecture · 3 → 4 · RESOLVED 2026-06-29 (scorecard §8 maturity 4 / impl 9); TD-03 concurrency round-trip CLOSED 2026-07-06 (implemented + deploy-gated, Conference-only, so impl holds 9)

  • (Low) The orphaned integration suite means soft-delete/concurrency/outbox/migration behaviors have no ADC-repo regression coverage. per-service integration tests restored (#14) exercise CRUD/auth/ownership against real per-service SQL DBs; migration drift + soft-delete fidelity now guarded.

Fix

  • Restore per-service integration tests (done via #14).
  • Migration model-drift gate: build-and-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 test: SoftDeleteFidelityTests (Conference integration tier) deletes an Event via the API, asserts it's hidden by the EF global query filter (404), and reads [Conference].[Event] directly to prove the row survives with IsDeleted = 1 (soft- not hard-delete). The fixture now exposes its ConnectionString for raw-table assertions.
  • Outbox-dispatch fidelity: OutboxFidelityTests (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:27 (Update_WithStaleRowVersion_ReturnsConflict) asserts the 409 inside the PR-gating MMCA.ADC.Integration.slnf (wording corrected 2026-08-23: the integration-tests job is a required PR check, PR-only at deploy.yml:389, not 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 extension point (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 every PR as a required check (wording corrected 2026-08-23).
  • 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.
  • Implementation half REOPENED 2026-09-04 (thirtieth-cycle re-score: scorecard §9 implementation 9→8, maturity 4 holds and the proposed 4→3 was rejected). First score against the rubric v2 "contract tests at the boundary" criterion: the OpenApiContractTests above assert well-formedness, a paths-count floor and three pinned resources, explicitly not a baseline diff (OpenApiContractTestsBase.cs:15), and no AsyncAPI-style async contract document exists. The maturity item stays closed; the work is TD-24 on the implementation band below.
  • 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 prodfixed 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 gate post-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 identity: DONE: ACR pull via the shared UAMI (admin password gone) and all runtime secrets moved to RBAC Key Vault (read via managed identity). Add a rollback path + post-deploy smoke gate → DONE. (ACR admin user disabled: no admin credential exists.)
  • Switch app→DB SQL auth to managed identity (pairs with #11): DONE (2026-06-28): useManagedIdentitySql=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 summary RESOLVED (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:638, job at :628, still in deploy.needs at :992; anchors refreshed 2026-08-31), so firefox and webkit run only on the nightly, where they are continue-on-error (e2e.yml:144). Thinner still since 2026-07-29 (re-verified 2026-08-01): the nightly matrix was replaced by alternating single-engine legs, two separate crons running Monday firefox and Thursday webkit (e2e.yml:49,:50, rationale :44-48), so each non-chromium engine is now exercised once a week rather than twice. Cross-engine verification is nightly-advisory, 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 (job at deploy.yml:628 with browsers: '["chromium"]' at :638, still in deploy.needs at :992; anchors refreshed 2026-08-31), so firefox/webkit now run only on the alternating single-engine nightly schedule (crons e2e.yml:49,:50), where e2e.yml:144 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:549,606,760) 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). Qualified 2026-09-01 (twenty-ninth-cycle re-score): scorecard §25 implementation 8→7, maturity 4 holds (the proposed maturity 4→3 was adversarially rejected: the enforcement leg is intact, automatic, and kept up with the new pages, ManagementRouteAuthorizationTests.cs:19 with the Activity namespace governed at :35 and MinimumGovernedPages 15 at :48, IdentityRouteAuthorizationTests.cs:16, both in MMCA.ADC.CI.slnf:43,:49 and run by the merge-gating build-and-test job at deploy.yml:284). The down-move is documentation drift, which the rubric scores on the implementation axis: adc-NavigationFlow.md (557 lines) has zero occurrences of /activities, /engage or speaker/qr, leaving 7 of the 53 routable @page files undocumented (/activities, /activities/create, /activities/{Id:int}, /conference/activities, /speaker/qr, /engage/sponsors/{SponsorId:int}, /engage/rooms/{RoomId:int}), the authorization enumeration at :532-534 omitting all of them, two nav items undescribed (Nav.Activities, Nav.SpeakerQr, ConferenceUIModule.cs:29,39), and no ADC-side drift gate (Common's NavigationContractTests parses Common's own embedded doc and is not a shared base). The pages landed in PRs #116 (2026-08-13) and #127 (2026-08-19); the doc was edited after them on 2026-08-22 (#123) without picking them up. #25 now carries a live row in the implementation band below (implPriority 4) with its lever recorded there

  • 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: DONE. ManagementRouteAuthorizationTests (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 at the 2026-07-02 re-score (the inbox then covered only 2 of 4 consumer services). Header refreshed 2026-07-23: since the 2026-07-11 TD-02 close, MessageBus:EnableInbox=true is live on all four consumer services (see the TD-02 item below), so the former "2 of 4" basis no longer holds; impl stays 9 on the remaining broker-tier gating nuance, and the category stays maturity 4 (protect)

  • No event-schema versioning; ID-dependent events published post-commit (intentional: the post-commit publish is how events carry DB-generated identities); broker round-trip tests excluded (still deferred: needs a RabbitMQ container).
  • Event-contract guard: IntegrationEventContractTests (architecture tier) reflects over 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:570, in deploy.needs at :829) and the WebVitals budgets are enforced inside the e2e-gate (§23's credit), but the k6 tier itself executes monthly/dispatch out of band (load-test.yml:18) and the Notification app stays pinned maxReplicas: 1 (infra/main.bicep:1447), 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 (checks at deploy.yml:526,583,642), so the k6 recency proof is bypassable-with-justification rather than unconditional (see Deliberate / accepted). Re-confirmed again 2026-07-28 (twenty-fourth cycle) and 2026-08-01 (twenty-fifth cycle) at M3/I8, substance unchanged both times; all anchors in this header were refreshed again on 2026-08-01 (load-freshness :570:606, deploy.needs :829:866, the Notification pin infra/main.bicep:1447:1530, refreshed again 2026-08-14 to :1616 (scale block) with its right-sizing rationale in the comment ending :1614, refreshed again 2026-08-23 to :1648 with the rationale at :1643-1647, the break-glass checks :526,583,642:562,619,678; the pin anchor refreshed again 2026-08-31 to :1591 with the rationale ending :1589, and deploy.needs :866:992). Rewritten 2026-09-01 (twenty-ninth cycle, HEAD 65bddd4b): the Notification single-replica basis is GONE and must not be restated. The hub now runs scale: { minReplicas: 1, maxReplicas: 2 } (infra/main.bicep:1596) on the injected backplane (:1516), unblocked by a verified cross-replica proof (Tests/Integration/MMCA.ADC.CrossService.IntegrationTests/CrossService/TwoReplicaHubFanOutTests.cs:49: a SignalR client held on replica B receives a push issued through replica A; green in nightly run 33500459363, taking the cross-service tier to 10 tests), with the unblock rationale at :1586-1595 recording that the cap returns to 1 if that test is ever deleted or skipped; there is no maxReplicas: 1 anywhere in main.bicep any more, so every older :1591/:1648/:1530/:1447 pin anchor above is history. Maturity still holds at 3 for the original structural reason (the k6 tier executes monthly cron/dispatch out of band, load-test.yml:18, and load-freshness checks recency, not execution: deploy.yml:756-806, FRESHNESS_DAYS: "35" at :764, in deploy.needs at :1054), and implementation holds at 8 because the closed fan-out lever is offset by the withdrawn IUiReadCache adoption (zero occurrences in ADC Source) and by a NEW open failure on the same criterion set: TD-21 below. Update 2026-09-04 (thirtieth cycle): TD-21 is CLOSED and the red-k6 negative is gone, but implementation still holds at 8 (a proposed 8→9 was adversarially rejected) on the IUiReadCache withdrawal plus a NEW negative on the same criterion: the only green capacity proof (run 33589806414, 04:10 UTC 2026-09-02) measured a topology that the same day's prod cost tier 1 replaced (ADC #173, commit 66de9341: infra/main.bicep:1267 halves Conference to 0.25 vCPU / 0.5 Gi, :1692 the Gateway), and the recency-only load-freshness gate (deploy.yml:756, FRESHNESS_DAYS: "35" at :764, in deploy.needs at :1054) never re-measures. Real new substance landed in the same window (Redis-backed output caching with broker-driven eviction, Source/Services/MMCA.ADC.Conference.Service/Program.cs:141,:250) and is credited inside the 8. Maturity basis unchanged: load-test.yml:23 monthly cron plus :14 dispatch, never in the deploy chain.

  • 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:1447, deliberate right-sizing rationale at :1443-1446; anchors refreshed 2026-07-28), so a verified two-replica hub fan-out remains the §12 impl 8→9 lever and this category stays open.

  • TD-21 (recorded 2026-09-01, under #12, effort S) · CLOSED 2026-09-02: the k6 capacity proof is GREEN again through the synthetic-traffic bypass (MMCA.Common v1.180.0, ADC PR #170 deployed b55e279b; dispatch run 33589806414: 30105 checks, 100% succeeded, http_req_failed 0.00%, p95 139.89 ms against the 800 ms budget; Store run 33589272010 green the same night), so load-freshness holds a fresh success and the 2026-09-05 deploy block never fired. Record of the failure as found at the re-score follows. The recency gate would have blocked every deploy from 2026-09-05. Today's scheduled run (load-test.yml, run 33500095682, event=schedule, 2026-09-01) FAILED: status is 200 840/21325, http_req_failed 96.06% against the threshold rate<0.01 (Tests/Load/k6/conference-read-load.js:50), exit code 99. Blocker (structural, not an app regression): the gateway per-client-IP edge limiter (Source/Hosts/MMCA.ADC.Gateway/appsettings.json:14-15, PermitLimit: 120, WindowSeconds: 60, landed 2026-08-18 in commit 79b3dcf8) rejects a single-runner k6 driving roughly 101 req/s from one IP. The last green run is 2026-08-01 (run 30688514601), and load-freshness is a 35-day recency check on the last successful run (deploy.yml:756-806, FRESHNESS_DAYS: "35" at :764) sitting in deploy.needs (:1054), so every ADC deploy fails from 2026-09-05 unless the run goes green or the skip_freshness_gates break-glass (with a mandatory skip_justification) is used. Resolution path (framework fix, chosen 2026-09-01 over an ADC-only workflow lift because Store's proof failed the same morning, run 33499906085, and because load-test.yml promises never to mutate production): MMCA.Common gains a secret-gated synthetic-traffic bypass on the edge limiter (a request whose X-Synthetic-Traffic-Key header matches a configured 32+ character secret takes the no-limiter partition on both chained limiters; off by default, constant-time compare; MMCA.Common PR #340, ADR-088 amendment), released as v1.180.0; ADC then bumps its pins, injects GatewayRateLimiting__SyntheticTrafficSecret into the gateway from Key Vault the same way it injects the SMTP password, sends the header from load-test.yml via a repository secret, and re-dispatches the run to restore a green proof before 2026-09-05. Effort: S in ADC (the framework half is its own PR). Shipped 2026-09-01/02 exactly along this path: Common #340 + release v1.180.0 + FACTS #342, ADC #170, Store #116, Helpdesk #96, ADR-088 amendment (Website #146).

  • (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; conditional since 2026-07-29 per TD-20). 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.

  • (impl-8 lever, recorded 2026-09-04, effort S) re-dispatch load-test.yml against the deployed 0.25 vCPU topology (infra/main.bicep:1267,:1692) and keep it green; a fresh proof on the right-sized services closes the stale-proof negative and re-opens the §12 8→9 case (the IUiReadCache withdrawal stays the other half, not to be re-adopted without the framework fixes recorded under Deliberate / accepted). The maturity lever is unchanged: an in-band capacity proof.

[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 projects RESOLVED: 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; conditional since 2026-07-29 per TD-20: a non-UI merge deploys without a CWV assertion), 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 (live in deploy.needs, deploy.yml:791, since 2026-06-30)

  • No budgets/alerts, no cost tags, no scheduled scale revert. (Baseline was already cost-minimal: all SQL Basic, all apps min1/max2; the conference surge had been reverted in Bicep. The gap was the absence of guards/attribution.)
  • Budget + cost alerts, Microsoft.Consumption/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 tags: commonTags (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. Live in deploy.needs since 2026-06-30 (phrasing refreshed 2026-07-23).
  • 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). Figure status (2026-08-14): the dollar amounts above are third-consecutive-cycle unverified (no billing read on 2026-07-28, 2026-08-01 or 2026-08-14), so they are recorded as written and must not be restated as re-confirmed. What WAS re-verified this run is the cost-driver topology behind them: Redis Enterprise Balanced_B0 (infra/main.bicep:864-869), Service Bus Standard (:709-714), and the per-service Container Apps and per-service SQL databases, all unchanged. 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 (65 as of 2026-07-23; 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.

[x] #33 · Developer Experience & Inner Loop · 3 → 4 (weight 2) · RESOLVED 2026-09-01 (twenty-ninth-cycle re-score: scorecard §33 maturity 4 / impl 9): both halves of TD-17 shipped in PR #162 (commit 7cc8d19c). The Service Bus emulator parity tier is now authoritative, not advisory: servicebus-emulator-smoke carries no continue-on-error (cross-service-tests.yml:153) under an "AUTHORITATIVE SINCE 2026-08-31 (TD-17)" header block (:126-137) whose :135 forbids re-adding one, and cross-service-freshness now selects a run in which BOTH cross-service and servicebus-emulator-smoke concluded success (deploy.yml:874), failing the deploy otherwise (:885), with the gate in deploy.needs (:1054) and required at :1089. The parity gap is also closed at the inner loop rather than documented: ADC_BROKER=servicebus swaps RabbitMQ for AddServiceBusEmulatorBroker (AppHost/Program.cs:91, :93-94) as an opt-in local profile, the default staying RabbitMQ (:86), which with stale README/docs prose is what holds implementation at 9 rather than 10. The basis recorded below is now historical: this tier must NOT be described as "weekday-nightly, advisory, rides no gate" any more. Prior history follows. 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:142), but it runs nightly and reaches the deploy only through the cross-service-freshness recency gate (deploy.yml:627, in deploy.needs at :829), which is itself bypassable via skip_freshness_gates (:642). 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. BASIS WEAKENED 2026-07-28 (twenty-fourth cycle) when the emulator job went dispatch-only, then PARTLY RESTORED and REWRITTEN 2026-08-01 (twenty-fifth cycle): the tier is back on the weekday nightly (cross-service-tests.yml:145-147, workflow_dispatch at :26 plus cron '0 6 * * 1-5' at :31, timeout-minutes: 10 at :149; anchors corrected 2026-08-14 from the drifted :144-146/:26,:30/:148) since 2026-07-29, and the 2026-07-28 "dispatch-only / no schedule" text is superseded. It still rides no gate: continue-on-error: true (:150), and cross-service-freshness keys off the cross-service job, not this one (:126-129; gate at deploy.yml:760, re-anchored 2026-08-31). The score holds at M3/I8 on that half alone, and the impl 8→9 candidacy was re-rejected a second time this cycle, because the local topology still diverges (the AppHost provisions RabbitMQ only) and an advisory nightly is not closure. The README sentence offered in support of the lift is itself inaccurate about the gate. Tracked as TD-17 below, CLOSED 2026-09-01; the "weekday-nightly, advisory, rides no gate" description above is retained as history only and is false as of 2026-08-31

  • 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.
  • TD-17 (recorded 2026-07-28, half closed 2026-08-01, CLOSED 2026-09-01 in PR #162) · the Service Bus emulator parity tier now gates the deploy. Proving anchors read this run: servicebus-emulator-smoke at cross-service-tests.yml:153 carries no continue-on-error (the only one left in the file is apphost-smoke at :204) under the "AUTHORITATIVE SINCE 2026-08-31 (TD-17)" header at :126-137, whose :135 forbids re-adding it; cross-service-freshness requires both cross-service and servicebus-emulator-smoke to have concluded success in the same run (deploy.yml:874), fails the deploy otherwise (:885), sits in deploy.needs (:1054) and is required at :1089; and the local parity profile exists at AppHost/Program.cs:91,:93-94 (ADC_BROKER=servicebus, opt-in, default RabbitMQ at :86). The prior open basis follows as history: the Service Bus emulator parity tier is back on a schedule but still gates nothing. The schedule half is DONE: servicebus-emulator-smoke runs on the weekday nightly again (cross-service-tests.yml:145 for the job, needs: should-run at :146 + if: needs.should-run.outputs.run == 'true' at :147, under the workflow's workflow_dispatch at :26 plus cron: '0 6 * * 1-5' at :31, timeout-minutes: 10 at :149), restored 2026-07-29. Anchors corrected 2026-08-14 (the recorded :144-146 / :26,:30 / :148 had drifted). The recorded blocker was wrong and is corrected here: the RESCHEDULED comment at :130-143 documents the real cause as per-test bus re-provisioning against an admin plane throttled at roughly 1 op/sec (IAsyncLifetime plus xUnit's per-[Fact] class instantiation), fixed by moving the bus to the collection fixture and putting wall-clock bounds on both startup phases. It was not the emulator's companion SQL image; that theory is withdrawn. What remains open: the tier is continue-on-error: true (:150) and rides no gate, because cross-service-freshness keys off the cross-service job, not this one (:126-129; gate job at deploy.yml:760, re-anchored 2026-08-31; the job/cron/timeout/continue-on-error anchors :145/:146/:149/:150 re-confirmed this run). So the Service Bus signal is advisory: informative, not authoritative. Caution: the "verified by three consecutive dispatches" line at :143 is a workflow comment, not run evidence read this cycle, so the §33 M3→4 / I8→9 candidacy stays unproven. Resolution path (executed 2026-08-31, verified 2026-09-01): the continue-on-error was dropped AND the job was added to the freshness gate, and the §33 lift was then proposed and confirmed on that evidence, so both axes moved (maturity 3→4, implementation 8→9). Everything above this sentence is the historical open record.

🔵 Implementation band (implementation <= 8, ranked by implPriority)

Added 2026-07-28 when the ledger gained its second ranked axis. Until then implementation gaps appeared only as unranked sub-bullets inside maturity items, so they were never scheduled against each other: the maturity index reached 97.8% while implementation sits at 85.6%. Ranked from the current scorecard (2026-09-04 thirtieth cycle, one implementation up-move, one down, one entrant): 14 categories, 40 gap points. §16 enters at implementation 5 and takes the top of the band alone at implPriority 8 (AI session scoring, scoreable under rubric v2 since it calls a model in production), §9 enters at implPriority 2 on the v2 contract-test criterion, and §4 leaves the band at implementation 9 (the Email value object inside the Event aggregate, ADC #177). The prior cycle (2026-09-01): §15 rose to 8 and dropped from the joint top to implPriority 2 (both of its effort-S hygiene levers shipped), §28 and §33 left the band entirely at implementation 9, and §25 entered it for the first time at implPriority 4 on documented-navigation drift. The 40 points are the scheduling gap to 9 on every category (the index gap to a full 810 is 121); the "90% attainable ceiling" framing used here before 2026-08-01 is retired, since a 10 is now awardable for an almost perfect implementation and the index reads directly against 100%. The prior record stands as history: eight rows were re-proposed on 2026-08-23 and all eight rejected (§5, §7, §15, §17 as a 9→10, §18, §21, §28, §31), then all ten re-proposals on 2026-08-31 were rejected (§5, §7, §12, §13, §15, §21, §23, §24, §27, §31), a fourth consecutive all-rejected cycle. The 2026-09-01 cycle broke that run: of its nine adjudications (§4, §5, §7, §12, §21, §22, §24, §25, §27) eight were still rejected lifts, but §15, §28 and §33 moved up on first-pass evidence and §25 moved down. The 2026-09-04 cycle: ten adjudications, §4 confirmed up, §9 and §16 corrected by the verifier, seven lifts rejected.

Four of these categories (§12, §16, §21, §22) also sit in the maturity band above and keep their existing item there; this band records only their implementation half. Levers are cited only where the ledger or scorecard already records one: an unnamed lever is named at the next re-score, never invented here.

implPriority # Category w Impl Recorded lever
8 §16 AI-Native Application Architecture 2 5 NEW at 5, entered the band 2026-09-04 (N/A→M2/I5): the AI session-scoring feature makes rubric v2's §16 scoreable, and it enters at the bottom of the Adequate band on two fully open red flags (no evaluation suite; untrusted text in the prompt, AnthropicScoringService.cs:187) plus a half-open third (cost is a log line, :269). Levers are named under #16 in the maturity band: TD-23 (injection handling + prompt version + cost metric, effort S-M, 5→7) then TD-22 (evaluation suite as a CI gate, effort M, the maturity lever and 7→8/9). The struck-through former §16 row below records the retired Maintainability & Evolvability definition
4 §22 Responsive & Cross-Browser 2 7 Entered the top block 2026-08-23 (impl 8→7), sole top row since 2026-09-01 alongside §25. Lever named from the down-move basis (replacing "not yet identified"): adopt the rubric's density-options criterion, which has zero adoption anywhere in ADC, and complete content reflow on the 17 non-DataGrid table pages, including the data-dense conference-day surfaces (the DataGrid pages already degrade to card lists). The chromium-only gate (deploy.yml:691) remains the maturity half (see the open #22 item above). Effort M
4 §25 Navigation & Information Arch 2 7 NEW at 7, entered the band 2026-09-01 (impl 8→7; maturity 4 holds and the proposed 4→3 was rejected, so #25 stays in the protect set and this is an implementation-only entrant). Lever (effort S): add the seven undocumented routes (/activities, /activities/create, /activities/{Id:int}, /conference/activities, /speaker/qr, /engage/sponsors/{SponsorId:int}, /engage/rooms/{RoomId:int}) and the two undescribed nav items (Nav.Activities, Nav.SpeakerQr, ConferenceUIModule.cs:29,39) to adc-NavigationFlow.md, both to its per-actor diagrams and to the authorization enumeration at :532-534. Optional second lever (effort M): an ADC-side navigation contract test modeled on Common's NavigationContractTests, so the doc cannot drift silently again (Common's parses its own embedded doc and is not a shared base)
3 §4 Domain-Driven Design 3 9 LEFT THE BAND 2026-09-04 (thirtieth cycle, impl 8→9, ADC #177 at HEAD f831b8b8): Event.OrganizerContactEmail is the shared Email value object (Event.cs:59) and its invariant is enforced inside Event.Create (:189) and Event.Update (:265), closing the primitive-obsession ground outright and the larger half of the aggregate-external-validation ground; the public-setter ground was closed in #152. Residual, recorded here and not as a TD because a category at M4/I9 has no band home: SponsorshipPacketUrl and TicketingUrl are still validated only in the Application layer (EventValidationRules.cs:77,:97, composed at :149-150, vs the aggregate's Result.Combine at Event.cs:195), and the cross-aggregate object navigations remain beside the by-ID references, private-set (Session.cs:71,:75, Activity.cs:58, Sponsor.cs:49). Both are the 9-not-10 polish. Struck through for the record; not counted in the band total. Prior record follows: Entered the band 2026-08-23 (impl 9→8; maturity 4 holds, so #4 stays in the protect set and this is an implementation-only entrant). Public-setter cross-aggregate navigations fixed in ADC #152 (2026-08-29) and re-verified 2026-09-01; the two remaining grounds are aggregate-external validation of Event's optional fields (email/URL rules living in EventValidationRules.cs:65 rather than in the aggregate, against the repo's own Sponsor.Create convention) and OrganizerContactEmail as a raw string? where the Email value object covers the same concept on User and Speaker. Which to schedule first is still not adjudicated. Do not promote the old "Money/Address VOs live in Common" nit into the lever: it was already priced into the prior 9
3 §7 Microservices Readiness 3 8 not yet identified (re-confirmed 2026-09-01 byte-identical: seven typed gRPC client registrations across four services, two bidirectional cycles)
3 §18 UI Architecture & Components 3 8 TD-16 under #18 above, re-measured 2026-09-01 and the pressure released: the high-water code-behind is PublicSessionDetail.razor.cs at 386 of the 400 cap (14 lines of headroom) and exactly one file now sits within 38 lines of the cap, down from nine, so the "flush at the cap / zero headroom" framing is retired. The category holds at 8 because the extraction is applied file by file rather than systematically. Effort S
3 §21 Accessibility (a11y) 3 8 not yet identified (the recorded SR-pass lever is the maturity half; axe is still E2E-only, chromium-only, non-PR and conditional)
2 §5 Vertical Slice Architecture 2 8 TD-19: the enforced architecture map covers 3 of the 4 modules SHIPPED and ticked 2026-09-01 (AdcArchitectureMap.cs:51-54 now registers Notification, so every map-driven fitness rule covers all four modules). Lever not yet identified, name it at the next re-score: the accepted layered-by-project hybrid (DTOs in Shared with horizontal mapper/validator folders) remains the recorded cap and is not itself a lever
2 §9 API & Contract Design 2 8 NEW at 8, entered the band 2026-09-04 (impl 9→8; maturity 4 holds and the proposed 4→3 was rejected, so #9 stays in the protect set and this is an implementation-only entrant). First score against the rubric v2 criterion "contract tests at the boundary" (ArchitectureEvaluationCriteria.md:316): the shared OpenAPI guard is explicitly not a baseline diff (OpenApiContractTestsBase.cs:15), asserting only well-formedness, a paths-count floor (:64) and pinned resource strings (:78; ADC subclass at Tests/Integration/MMCA.ADC.Conference.IntegrationTests/Contract/OpenApiContractTests.cs:17), so a renamed field or a changed status code passes; and no AsyncAPI-style async contract document exists, the seven events being frozen only inside IntegrationEventContractTests.cs:9. Lever: TD-24 below (effort M). The #9 maturity item above stays closed; the "snapshot-guarded" wording it inherited from the scorecard was corrected this cycle
2 §12 Performance & Scalability 2 8 see the open #12 item above. Notification pinned maxReplicas: 1 CLOSED 2026-09-01: the hub scales to maxReplicas: 2 (infra/main.bicep:1596) on a verified cross-replica fan-out (TwoReplicaHubFanOutTests.cs:49). The remaining basis is the out-of-band capacity proof, which is now RED: TD-21 (the 2026-09-01 k6 run failed 96.06% against the gateway edge limiter and the 35-day recency gate blocks deploys from 2026-09-05), plus the standing withdrawal of the IUiReadCache opt-in (PR #161) on the caching criterion
2 §15 Best Practices & Code Quality 2 8 Was implPriority 4 at impl 7; rose to 8 on 2026-09-01 when BOTH effort-S hygiene levers SHIPPED (PR #162): delete the expired audit suppression GHSA-2m69-gcr7-jv3q DONE, Directory.Build.props now carries zero NuGetAuditSuppress items and no GHSA id at all (and the one live high advisory is remediated by a patched SSH.NET 2026.0.0 pin at Directory.Packages.props:83 rather than suppressed); justify-or-drop the undated global NoWarn codes DONE, the global line is CS1591;EXTEXP0001;S8970 at :31 with each code dated and justified (:16-21, :22-26, :27-30) and RMG020 narrowed into an .Application-scoped PropertyGroup (:57, rationale :50-55). The structural half TD-18 below is the only remaining 8→9 lever (plus the minor residual that the four-code NoWarn list is duplicated across five test csproj files against the centralization intent at Directory.Build.props:40-48). Store should still be swept for the same MAUI-graph gap
2 §16 Maintainability & Evolvability 2 8 Retired 2026-09-04 (rubric v2, ADR-110): §16 is now AI-Native Application Architecture and N/A for this repo. The former category's coupling and tech-debt criteria score under #34, lockstep upgrades under #32, onboarding under #33. Struck through for the record; not counted in the band total.
2 §23 Front-End Performance 2 8 the WASM code-split / image sub-item recorded under #23
2 §24 Forms, Validation & UX Safety 2 8 Two levers named 2026-08-01; lever (b) is now CLOSED. (a) client validation does not mirror the server's cross-field and format rules in three places, which is the category's first criterion and its first red flag: still open and still the reason the 8→9 was rejected on 2026-09-01. (b) the form-level error summary is present on 9 of the 21 MudForm instances CLOSED 2026-09-01: all 21 of 21 MudForm files now render an <ErrorSummary>. Effort M for the remainder
2 §31 Cost Efficiency / FinOps 2 8 Lever restated 2026-08-01 (it was recorded in the scorecard row, not here): automate the conference-day surge and its revert into a scheduled scale event, rather than a manual play with a drift alarm. This is the rubric's "reversible scale events" criterion, still unmet in its exact terms, and it is why the 8→9 was rejected on 2026-08-01 and again on 2026-08-14. Re-confirmed unmet against cost-guard.yml: :4 still describes the conference-day surge as a manual scale-up, :12 is the weekly cron and :17 the workflow_call entry, :59 only reads maxReplicas, and :83 prints a manual reset instruction, so no scheduled or automated surge-and-revert exists. Effort M. Do not confuse it with TD-15, the separate un-verified cost-collapse item under #31
1 §27 Internationalization (i18n) 1 8 DEFERRED 2026-07-28, do not re-propose without new evidence (it was re-proposed anyway on 2026-08-01, 2026-08-31 and again on 2026-09-01 and rejected a fourth, fifth and sixth time, which is the cost this entry exists to prevent; each run has surfaced evidence moving against the lift rather than for it). Pseudo-loc breadth: PseudoLocalizationTests.cs:51-56 covers 3 public pages (public by design, :31) of 53 routable @page files. Proposed and rejected in six cycles (21st, 22nd, 24th, 25th, 28th, 29th; the 23rd rejected §12/§21, not this) on byte-identical evidence: the tier is untouched since c5e6f653 on 2026-07-11 and no .resx has landed since 2026-07-20. Worth 1 weighted point of 800 against authenticated-login plumbing plus expansion assertions on roughly 45 pages, the weakest cost-to-benefit ratio on either band. Re-open triggers and full rationale in Deliberate / accepted below A seventh rejection followed on 2026-09-04 (thirtieth cycle): the first pass proposed 9 again and the verifier held 8 on the still-live culture-aware-formatting red flag and the 3-of-53 pseudo-localization coverage.

Σ implPriority = 40 across the 14 live rows above (8+4+4+3+3+3+2+2+2+2+2+2+2+1; the struck-through §4 row that left at implementation 9 and the retired former-§16 row are not counted), re-summed 2026-09-04 at the thirtieth-cycle re-score; ties are broken by weight descending, then category number ascending.

Tactical sub-items on this band (§15, §5, §9 and §28 have no maturity-band item to nest under: all four score maturity 4 and sit in the protect list, so their TD-NN items live here with their rows; §28 left the implementation band on 2026-09-01 but TD-20 stays recorded here because half of it is still open):

  • TD-18 (recorded 2026-07-28, under §15, effort L) · the MAUI app is outside every CI build and outside the CI-audited dependency graph. MMCA.ADC.CI.slnf lists only UI.Web (:25) and UI.Web.Client (:26); no workflow installs the maui-android workload, so MMCA.ADC.UI is never compiled in CI and its analyzers, TreatWarningsAsErrors and its own NoWarn CA5392 (Source/Hosts/UI/MMCA.ADC.UI/MMCA.ADC.UI.csproj:151; anchors refreshed 2026-09-01 from the drifted :143/:142) are review-enforced only. The gating vulnerable-package scan runs against CI.slnf too (deploy.yml:465, audit step :456-476; anchors refreshed 2026-09-01 from the drifted :416/:425), so the MAUI graph is the one graph never audited. Re-confirmed open 2026-09-01, with one partial advance and one clause retired: the Directory.Build.props:8-12 System.Private.Uri suppressions this item used to cite no longer exist (that file now carries zero NuGetAuditSuppress items and no GHSA id), so the "the suppressions exist for a graph never audited" framing is retired; and a new .github/workflows/maui-audit.yml audits the MAUI graph on a weekly cron plus dispatch (:36), but it is android-only (:19), its header claims only the supply-chain half is closed (:14), and it has never run (gh run list --workflow=maui-audit.yml returns nothing), so the audit-only partial recorded below is drafted rather than proven. This is what caps §15 at implementation 8 now that the two effort-S hygiene items have landed. Blocker (and why this is recorded, not scheduled): adding a MAUI leg means installing the maui-android workload on a runner, which is a multi-minute install on every run and cuts directly against the deliberate 2026-07-18 Actions-minute reduction that also unscheduled the emulator tier (TD-17) and cut the E2E gate to chromium (#22). A cheaper partial is auditing the MAUI graph alone (dotnet list package --vulnerable over that project, no build), which would close the supply-chain half without the workload cost. Resolution path: either the cheap audit-only step, or a scheduled (not per-PR) MAUI build leg; then re-propose §15 impl 8→9. Do not describe §15's maturity-4 enforcement as repo-wide while this is open: it is CI.slnf-wide.
  • TD-19 (recorded 2026-08-14, under §5, effort S) · CLOSED 2026-09-01 (PR #162): the enforced architecture map now covers all four modules. AdcArchitectureMap.cs:51-54 carries the Module("Notification", ...) entry, so MMCA.ADC.Notification.Application / .API / .Shared are inside every map-driven fitness rule (slice cohesion, layer dependency, transport-at-the-edge). The §5 8→9 lift was still rejected this cycle, because the accepted layered-by-project hybrid is the remaining cap and is not itself a lever. The prior open record follows as history: the enforced architecture map covers 3 of the 4 modules. AdcArchitectureMap.DefineLayers() declares Framework + Identity + Conference + Engagement only and carries no Module("Notification", ...) entry at all (Tests/Architecture/MMCA.ADC.Architecture.Tests/AdcArchitectureMap.cs:12-43; its doc comment at :4-5 names only Identity, Conference and Engagement), so MMCA.ADC.Notification.Application / .API / .Shared sit outside every map-driven fitness rule (slice cohesion, layer dependency, transport-at-the-edge) even though all three build in the CI gate (MMCA.ADC.CI.slnf:30-32). This is the same omission the 2026-08-01 and 2026-08-14 §5 8→9 rejections cited, and it is the named lever for that row. Blocker: none, this is scheduled work. Resolution path: add the Notification module entries to the map (Application, API and Shared anchors, mirroring the Identity/Conference/Engagement blocks) and fix whatever the rules then catch. Effort: S. The deliberate layered-by-project hybrid stays the accepted impl-8 cap and does not cover this: an enforcement-coverage gap is not an accepted trade-off.
  • TD-20 (recorded 2026-08-23, under §28, effort S-M) · the deploy-gating chromium E2E/axe/CWV suite is CONDITIONAL, not unconditional. e2e-gate runs only when the diff is UI-affecting (if: github.event_name != 'pull_request' && needs.changes.outputs.ui == 'true', deploy.yml:688, job at :677, rationale :682-687; anchors refreshed 2026-09-01 from the drifted :635/:628/:630-634), and the deploy job explicitly accepts a skipped gate (needs.e2e-gate.result == 'success' || needs.e2e-gate.result == 'skipped', :1092; deploy.needs at :1054). A backend-only, infra-only or script-only merge therefore reaches production with no browser run at all: no chromium E2E, no axe scan, no Core Web Vitals assertion. Until this cycle the conditionality was unrecorded in ADC governance (no hit for the ui scoping anywhere in the ledger or scorecard), while several entries called the gate unconditional; that language is now qualified in place (#12/#21/#23/#28). Blocker (deliberate): the 2026-07-29 Actions-minute saving; the workflow names the post-deploy smoke gate as the intended backstop (inside the rationale at deploy.yml:630-634). Resolution path: either add a cheap UI-independent smoke leg that always runs, or accept and record the conditionality permanently; in both cases keep the ledger's gate claims accurate. Effort: S-M. Paired with the Deliberate / accepted amendment below. PARTIAL as of 2026-09-01, not closed: the "a code deploy can reach production with zero test execution" half is shut by the new backend-test-gate (deploy.yml:394-396, whose if is the exact complement of e2e-gate's, in deploy.needs at :1054 and required at :1093, running the CI.slnf unit + architecture + bUnit tier with no Playwright browsers). The browser half is unchanged and stays open: e2e-gate is still ui-scoped (:688) and deploy still accepts a skipped gate (:1092), so a backend-only, infra-only or script-only merge still reaches production with no chromium E2E, no axe scan and no Core Web Vitals assertion. The zero-visual-regression half of §28's lever closed separately (markup-snapshot tier, see #28 above), which is why §28 moved to implementation 9 while this item stays open.
  • TD-24 (recorded 2026-09-04, under §9, effort M) · the API contract guard cannot fail on a breaking change, and the async contract has no published document. OpenApiContractTestsBase.cs:15 states there is no committed snapshot and the assertions run against the live document; the checks are well-formedness, a paths-count floor (:64) and case-insensitive presence of pinned resource strings (:78), with ADC pinning MinimumPathCount=10 and /Events, /Sessions, /Speakers (Tests/Integration/MMCA.ADC.Conference.IntegrationTests/Contract/OpenApiContractTests.cs:17). A renamed property, a changed status code or the removal of a non-pinned route group all pass. The seven integration events are frozen byte-exactly in the arch tier (Tests/Architecture/MMCA.ADC.Architecture.Tests/Contracts/IntegrationEventContractTests.cs:9) but documented nowhere alongside OpenAPI (a case-insensitive repo search for AsyncAPI returns no files). Blocker: the guard base lives in MMCA.Common (Source/Hosting/MMCA.Common.Testing/Conformance/OpenApiContractTestsBase.cs), so a baseline-diff mode is a framework change first (extract-to-Common rule) and an ADC adoption second. Resolution path: add a committed per-service OpenAPI baseline with a normalized diff assertion to the Common base (opt-in, breaking-change classes only), adopt it in the four ADC integration projects, and generate an AsyncAPI document from the frozen event list next to /openapi/v1.json. Effort: M (S in ADC once the Common half ships). Closing it re-opens the §9 8→9 case.

🟢 Resolved 2026-07-25 (performance program 2)

Second evidence-led performance pass over Common/ADC/Store. ADC's share shipped as three PRs plus the v1.127.0 framework sweep.

  • Output cache shared across replicas. Conference registered AddOutputCache with no store while running maxReplicas: 2, so every EvictByTagAsync reached only the replica that handled the mutation: the other served the pre-edit schedule/speaker payload for the full 5-minute TTL, and each replica filled its own copy, doubling cold reads against the Basic-tier database. Redis was already provisioned and wired as IDistributedCache. ADR-040's original trade-off (per-replica in-memory accepted) is superseded; see its 2026-07-25 amendment.
  • The uncached anonymous junction/lookup reads are cached and evict properly. CategoryItems (hit on every PublicSessionDetail view via CategoryItemLookupService, so each view was an uncached full-table read), SessionCategoryItems, SpeakerCategoryItems and EventSpeakers now carry their parent's policy and evict both parents' tags on mutation, which they previously could not do at all (no IOutputCacheStore was injected). SessionSpeakers already evicted correctly and only needed the attribute. Correction to the deferred item, which listed seven controllers: EventQuestionAnswers and SessionQuestionAnswers are [Authorize] with per-caller BR-8 scoping, so caching them would be a correctness bug. Five, not seven.
  • AI scoring stopped flushing the whole public surface. SessionScoringProcessor evicted the root conference tag twice per run, and every Conference policy carries it, so an organizer starting a scoring run during the event emptied events/speakers/rooms/categories/questions along with the sessions it changed. Now conference:sessions.
  • Bookmark counts no longer served stale for 5 minutes. The count changes on an Engagement mutation, in another process with no handle on Conference's cache store, so no eviction can reach it from either side and speakers (neither Organizer nor ContentEditor) got no admin bypass. Moved to a 60s policy: a short TTL is the only lever available from this side.
  • Attendee feedback out of the public payloads. Session.SessionQuestionAnswers and Event.EventQuestionAnswers were [Navigation], so includeChildren=true shipped per-attendee feedback on the session grid (every page, every user), session detail, the speaker dashboard, and the events call that exists only to build a room-name dictionary. Those collections grow with attendance, not with the schedule, and they are the one child set here that is not public data.
  • Speaker session lists filtered server-side. PublicSpeakerDetail, the organizer SpeakerDetail and SpeakerDashboardService each fetched the entire session catalog with all children and filtered in memory; the dashboard also appends a cache-bust by design, so every dashboard load was a full uncached catalog read. A virtual SpeakerId filter on the paged endpoint (the SpeakersController EventId precedent) resolves the SessionSpeaker join to a Session.Id IN (...) specification, ANDed with the BR-132 public filter, never substituted for it.
  • LivePoll(SessionId, Status) indexed. GetOpenPollsHandler filters on both, which is the session Live page and presenter view read once per attendee per structural poll event, and only EventId was indexed.
  • Prerender double-fetch guarded on PublicSessionDetail and PublicSpeakerDetail.
  • Load and CWV coverage. conference-read-load.js exercised only includeChildren=false paths, which is why the feedback-payload regression was invisible to load evidence; it now also reads the includeChildren=true shapes with a payload-size growth tripwire. /conference/sessions, the heaviest public surface, gained a CWV budget.

Deferred from that program (record the choice)

  • TD · Batch the feedback submit. EventFeedback/SessionFeedback loop one HTTP POST per answered question over a 10-question set, so one button press is 10 sequential Gateway-to-Conference round trips, each its own transaction, outbox write and audit stamp. Needs a batch upsert command and endpoint.
  • TD · Session-scoped live-poll management endpoint. SessionLive fetches event-wide polls and filters client-side. Not a type-compatible swap: GetEventPollsAsync returns LivePollDTO while the session-scoped call returns LivePollResultsDTO, and the event list is organizer-only today with speakers deliberately falling back on 403, so it needs an authorization decision too.
  • TD · GetOpenPollsHandler issues two queries per poll. The same N+1 shape the SessionQuestionViewBuilder fix removed, left one file over. Theoretical at ADC's scale (1-3 open polls), so recorded rather than fixed.

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). Scoping clause (2026-08-14): this accepted hybrid is the impl-8 cap, and it does not absorb TD-19. The absence of MMCA.ADC.Notification.* from AdcArchitectureMap.cs:12-43 is an enforcement-coverage gap and schedulable work, not an accepted trade-off.
  • #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; amended 2026-08-01): deploy.yml's e2e-gate invokes one browser leg instead of three (the job is at deploy.yml:628 with browsers: '["chromium"]' at :638; anchors refreshed 2026-08-31 from the drifted :531 / :541, substance re-confirmed and the job is still in deploy.needs at :992); firefox/webkit cross-engine coverage moved to the nightly e2e.yml, where continue-on-error (e2e.yml:144, refreshed from :131) keeps them advisory. Amendment (2026-07-29, recorded here 2026-08-01): the nightly was thinned again to ALTERNATING single-engine legs. Two separate crons now run Monday firefox and Thursday webkit (e2e.yml:49,:50, rationale :44-48, the leg chosen from the cron string that fired), so each non-chromium engine is verified once a week instead of both engines twice a week. This is a further deliberate CI-minute choice, recorded with the same shape as the parent entry; the earlier "Mon/Thu nightly matrix" phrasing used here and under #22 implied both engines on both nights and has been corrected. Scoring consequence: none beyond the existing one, since §22 already sits at M3/I8 on the chromium-only gate. The alternating schedule makes the nightly signal thinner, not the gate weaker. Recorded as a deliberate cost choice, with its scoring consequence stated plainly: it costs §22 its maturity 4, so the category reopens at maturity 3 (see #22; implementation dropped separately to 7 on 2026-08-23 on the density/reflow gaps). 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. Second amendment (2026-07-29 change, recorded here 2026-08-23): the gate is now also CONDITIONAL on the change set. e2e-gate runs only when the changes job's ui output is true (deploy.yml:688, job :677, rationale :682-687; anchors refreshed 2026-09-01) and deploy treats a skipped gate as pass (:1092), so backend-only, infra-only and script-only merges deploy with no browser, axe or CWV run at all; the workflow names the post-deploy smoke gate as the accepted backstop. Same shape as the parent entry: a deliberate Actions-minute trade-off with its consequence stated plainly, paired with TD-20 as the work that would restore an unconditional signal. Third amendment (2026-09-01): the deploy is no longer untested when this gate skips. A complementary backend-test-gate job now runs the CI.slnf unit + architecture + bUnit tier (no Playwright browsers) on exactly the deploys e2e-gate skips (deploy.yml:394-396, its if the exact complement of e2e-gate's, in deploy.needs at :1054, required at :1093), so one of the two always runs on a code deploy. The chromium-only fact (browsers: '["chromium"]' at :691) and the ui-scoped/skipped-gate facts above are unchanged and still true: what closed is the "zero test execution" hole, not the browser-coverage one.
  • 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 (declared at deploy.yml:13-18, with the per-gate checks at :562, :619, :678; anchors refreshed 2026-08-01 from the drifted :526,583,642, substance re-confirmed, and the gate jobs themselves are dr-freshness :549, load-freshness :606, cross-service-freshness :760 alongside cost-guard :616; the cross-service and cost-guard anchors refreshed 2026-08-31), 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.
  • Service Bus emulator smoke is advisory by design RETIRED 2026-09-01: this is no longer a deliberate choice, it is closed work. The tier became authoritative on 2026-08-31 (PR #162, TD-17 CLOSED): servicebus-emulator-smoke carries no continue-on-error (cross-service-tests.yml:153) under an "AUTHORITATIVE SINCE 2026-08-31 (TD-17)" header that forbids re-adding one (:126-137, :135), and cross-service-freshness requires both broker jobs to have concluded success (deploy.yml:874), failing the deploy otherwise (:885), in deploy.needs (:1054) and required at :1089. So the load-bearing clauses of the entry below ("advisory by design", "nothing gates on it", "§33 still holds M3/I8", "must not be described as gating") are all false as of 2026-08-31 and are kept only as history; §33 closed at maturity 4 / implementation 9 on 2026-09-01. The skip_freshness_gates break-glass that applies to this gate stays recorded in the separate freshness-gate entry above. Historical record follows. (Recorded 2026-07-24 as "unscheduled", reconciled 2026-07-28, REWRITTEN 2026-08-01 because the code now says the opposite): the §33 broker-parity tier was cut to dispatch-only on 2026-07-24, and it was restored to the weekday nightly on 2026-07-29 (cross-service-tests.yml:145 for the job, needs: should-run at :146 + if: needs.should-run.outputs.run == 'true' at :147, under workflow_dispatch :26 + cron: '0 6 * * 1-5' at :31, timeout-minutes: 10 at :149; anchors corrected 2026-08-14). The RESCHEDULED comment at :130-143 also records a different root cause than the 2026-07-24 entry claimed: per-test bus re-provisioning against an admin plane throttled at roughly 1 op/sec (IAsyncLifetime plus xUnit per-[Fact] class instantiation), fixed by hoisting the bus to the collection fixture and wall-clock bounding both startup phases. The "floating companion SQL image" blocker text is withdrawn, and the "no schedule / dispatch-only" framing is deleted. What survives as the deliberate choice: the tier is continue-on-error: true (:150) and advisory by design, and nothing gates on it, since cross-service-freshness keys off the cross-service job (:126-129, gate at deploy.yml:760, re-anchored 2026-08-31). So §33 still holds M3/I8 on the no-gate half alone, and this tier must not be described as gating. Paired with TD-17 (now half closed), which is the work that would make it authoritative. Same shape as the chromium-only entry above: a trade-off, not a closure.
  • Pseudo-localization breadth DEFERRED (§27, recorded 2026-07-28 after a third rejection): the §27 implementation 8→9 lever, broadening PseudoLocalizationTests beyond its three public pages (PseudoLocalizationTests.cs:51, public by design per :31) across the authenticated authoring surface, is adjudicated deferred rather than open. Rationale stated plainly: it is worth 1 weighted point of 800 (weight 1, one implementation rung) and costs authenticated-login plumbing plus text-expansion and overflow assertions across roughly 50 of the 53 routable @page files (denominator re-counted 2026-08-31 from the stale 49), the weakest cost-to-benefit ratio on either band. The identical proposal has now been adversarially rejected in seven cycles (21st, 22nd, 24th, 25th, 28th, 29th, 30th) against byte-identical evidence: the tier is untouched since c5e6f653 (2026-07-11) and no .resx has landed since 2026-07-20, so each cycle re-spent an adversarial verify pass to reach the same conclusion. The 2026-08-01 pass additionally found a citable culture-aware-formatting violation that was not previously recorded, so the fresh evidence points away from the lift, not toward it. §27 keeps its implementation-band row at implPriority 1 (band membership is numeric, implementation <= 8), but the lever is not to be re-proposed without new evidence. Re-open triggers: a second locale beyond es, any RTL locale, or a reported layout regression on an authenticated page. Maturity 4 is unaffected and remains doubly CI-gated (TranslationCompletenessTests + LocalizedTextConventionTests, both in MMCA.ADC.CI.slnf:58, run at deploy.yml:284; anchor refreshed 2026-08-31 from the drifted :219).
  • BR-130 room double-booking overlap check accepted as a SOFT guard (recorded 2026-08-01, BugHunt M42): SessionRoomScheduling.ValidateRoomAssignmentAsync is a read-then-write advisory check with no transaction, lock, or DB exclusion constraint tying check to write, so two concurrent organizer writes for the same room with overlapping windows can both pass. Accepted rather than hardened, with each alternative rejected on evidence at the 2026-08-01 BugHunt verification: a transactional re-check cannot close the race below SERIALIZABLE (and with no index on (RoomId, StartsAt) that isolation escalates to key-range/table locks across the Sessionize import path); IDistributedLock's own contract forbids sole-guard use on a correctness invariant and it silently degrades to the in-process implementation when Redis is absent while Conference scales to maxReplicas: 2; sp_getapplock needs an EF/SqlClient reference the Application layer forbids (the same layering constraint that produced CreateSessionHandler's message-based collision detection). The spec's BR-130 text promises only the cross-event room check (422); the overlap guard is code-only, organizer-gated, milliseconds wide at the 2026 load, and a double-booking is repairable by editing either session. Documented in the class XML doc plus a SOFT note at the ExistsAsync call (ADC PR #94; same shape as the BR-231 soft-cap precedent). No scoring consequence claimed. Re-open triggers: organizer concurrency materially above today's handful, a real double-booking incident, or a cheap DB-level range-exclusion capability appearing.
  • §16 is scored, not N/A (recorded 2026-09-04): rubric v2's "N/A in all three repos" claim (ADR-110) does not hold for ADC: AI session scoring has called a model in production since 2026-04-04, so the category is scored (M2/I5) and ranked on both bands (#16 above, TD-22/TD-23). The former §16 Maintainability & Evolvability criteria still score under #34 (coupling, tech-debt register), #32 (lockstep upgrades) and #33 (onboarding). The ADR-110 wording is a Website-side correction for /update-adrs, not scheduled work here; the IUiReadCache non-adoption recorded under #12 is likewise unchanged.
  • 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:553, 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. Update (2026-07-28 twenty-fourth-cycle full re-score, pin v1.131.0, HEAD 2ec77796): one score moved, and it moved down. §15 Best Practices & Code Quality implementation 8→7 (weight 2), which takes it to the top of the implementation band at implPriority 4; maturity holds 4, so #15 stays in the protect set and the maturity band is unchanged. A down-move is not a FLAG: it is a CONFIRMED move, adversarially verified, on three gaps read fresh this run (an audit suppression expired by its own written removal condition, three undated global NoWarn codes, and the MAUI project outside every CI build and outside the CI-audited graph). The single FLAG this cycle is §27 (M4/I8 verified non-move): a first pass proposed impl 8→9 for the third time and the adversarial pass rejected it on byte-identical evidence, correcting the score back to the prior values. Because the corrected values equal the prior ones, all 34 categories are evidence-backed this run even though the indices are labeled "33 rescored + 1 prior". That lever is now adjudicated DEFERRED with its cost and re-open triggers recorded above, so it should not return as a candidacy. §12/§21/§22/§33 return plain CONFIRMED at M3/I8, and the #33 re-confirmation rests on a weaker basis than last cycle (its parity tier is now dispatch-only, TD-17). Also re-rejected: #24 impl 8→9 and #33 M3→4 / I8→9; #6 and #30 sit at I9, already at the scheduling target of 9, so their recorded "9→10" candidacies are out of scope for both bands. Update (2026-08-01 twenty-fifth-cycle full re-score, pin v1.135.0, HEAD 995a7886): no score moved on either axis, and this cycle produced the largest FLAG set yet: six, every one of them a proposed implementation lift, every one rejected against current source. §5 8→9 rejected (the rubric's first §5 criterion wants the DTO in the slice; ADC's live in the Shared assembly with horizontal mapper/validation/specification folders, the layered-by-project hybrid is unchanged, and AdcArchitectureMap.cs:12-44 omits MMCA.ADC.Notification.Application, so enforcement covers 3 of 4 modules). §13 9→10 rejected (three ENABLED production alerts have no runbook triage section and sit outside the pairing gate's scope, including the sev-1 gateway-availability alert at infra/main.bicep:481; that is an unmet criterion, not the trivial polish the recalibrated top rung allows). §24 8→9 rejected (the named bUnit lever shipped and is CI-gated, but client validation does not mirror the server's cross-field and format rules and the error summary reaches 7 of 15 MudForm forms; both are now named as §24's levers). §27 8→9 rejected a fourth time on byte-identical evidence plus one new culture-formatting violation. §31 8→9 rejected (the surge/revert automation is not pulled). §33 8→9 rejected a second time (the AppHost provisions RabbitMQ only, and the restored Service Bus nightly is continue-on-error and gates nothing). Six rejections and zero moves is not a stalled cycle: it is six categories each sitting one criterion short, with the criterion now named in the band for five of them (§7, §16, §21, §22, §25, §28 remain "lever not yet identified"). A FLAG is a held score, not a closure, and none of these six changed band membership. Update (2026-08-14 twenty-sixth-cycle full re-score, pin v1.152.0, HEAD 19021d93): no score moved on either axis and the FLAG set grew to eight, every one a proposed lift, every one rejected against current source. §5 8→9 rejected (the DTO-in-the-slice criterion is still unmet and AdcArchitectureMap.cs:12-43 still omits the Notification module: that half is now named as TD-19). §7 8→9 rejected (the bidirectional sync-gRPC red flag did not close, it broadened to a second pair, Identity-Notification, across 7 sync client registrations in 4 services). §12 M3→4 rejected (zero commits touched load-test.yml, deploy.yml or Tests/Load/ since the prior cycle's HEAD, so the out-of-band capacity proof behind a recency-only gate is byte-for-byte intact). §13 9→10 rejected a second time (three ENABLED production alerts still carry no runbook triage section, including the sev-1 gateway-availability alert, anchor refreshed infra/main.bicep:481:496-502; §13 sits at I9, outside both bands). §15 7→8 rejected (all three downgrade grounds intact, and the expired SQLite suppression is further past its own removal condition now that ADC pins v1.152.0). §23 8→9 rejected (WASM code-split and image optimization, the category's own named lever, are both still open). §28 8→9 rejected (the genuine new state-management bUnit coverage is a within-band improvement, not a band change). §31 8→9 rejected a second time (the conference-day surge is still manual with a manual reset instruction and no automated revert). A FLAG is a held score, not a closure, and none of these eight changed band membership. Update (2026-08-23 twenty-seventh-cycle full re-score, pin v1.160.0, HEAD 96f0919a): two scores moved, both down, both CONFIRMED moves adversarially verified rather than FLAGs: §4 implementation 9→8 (public-setter cross-aggregate navigations, aggregate-external validation of Event's optional fields, primitive obsession on OrganizerContactEmail; the prior row's citations had all drifted and the fresh read placed the substance in the Strong band) and §22 implementation 8→7 (zero density-option adoption plus partial content reflow on the 17 non-DataGrid table pages, which names the lever this band had carried as "not yet identified"). The FLAG set held at eight, every one a proposed lift, every one rejected: §5 8→9 rejected a third time (DTOs and horizontal validators still outside the slice, the enforced validator rule exempting exactly the population that exists, ArchitectureRules.Slices.cs:38-39; the forgot-password vertical is fresh proof the hybrid still edits switchboards; TD-19 still open). §7 8→9 rejected (the synchronous-coupling red flag broadened rather than closed). §15 7→8 rejected a second time (all three downgrade grounds byte-intact; the expired suppression now twenty-five releases past its removal condition). §17 9→10 rejected (no CI/CD substance changed since the prior basis commit; the SQL public-network-access cap is verbatim open; the tightened 3d/keep-3 ACR purge narrows the rollback image window rather than widening it). §18 8→9 rejected (the cap-pressure gap WIDENED: eight code-behinds within 38 lines, two at 398; TD-16). §21 M3→4 and I8→9 both rejected (the SR-pass placeholder is still empty at adc-ACCESSIBILITY-SCREENREADER-PASS.md:62, and four routable pages shipped 2026-08-19 with no axe coverage: a new gap, not a lift). §28 8→9 rejected (zero visual-regression tests with the shared MarkupSnapshot helper unused, and the E2E layer is a conditional deploy gate, not a merge gate: named as TD-20). §31 8→9 rejected a third time (cost-guard.yml byte-unchanged since caf31e09; the surge is still a manual play with a manual reset). A FLAG is a held score, not a closure; the only band-membership changes this cycle came from the two confirmed down-moves. Update (2026-08-31 twenty-eighth-cycle full re-score, pin v1.175.0, HEAD b04b3a3e): no score moved on either axis and the FLAG set grew to ten, every one a proposed lift, every one rejected against current source. §5 8→9 rejected a fourth time (DTOs still in Shared with horizontal mapper/validator folders, the map still omitting Notification per TD-19, and EventsController.cs a 14-endpoint switchboard). §7 8→9 rejected (seven typed gRPC client registrations across all four services with two bidirectional cycles, Conference-Engagement and Identity-Notification; only a proto field addition and lock bumps touched the Contracts projects since the prior HEAD). §12 M3→4/I8→9 rejected (both prior caps byte-intact, plus the fresh IUiReadCache withdrawal in PR #161 as a new negative on the caching criterion). §13 9→10 rejected a third time (the three unpaired ENABLED alerts, including the sev-1 gateway alert, now at infra/main.bicep:474-498, still outside the pairing gate's parse scope by construction, ObservabilityConventionTestsBase.cs:109). §15 7→8 rejected a third time (all three grounds byte-intact; ground (1) strengthened at pin v1.175.0). §21 M3→4 rejected (the SR-pass placeholder still empty; the axe suite rides the conditional non-PR e2e-gate and ADC has no in-process axe harness in the CI.slnf tier; substance strengthened to 100 aria attributes across 42 files). §23 8→9 rejected (WASM code-split and image optimization still open; the v1.175.0 grid-virtualization opt-in overridden nowhere and the read-cache adoption withdrawn; the memoized event lookup and WASM token pre-hydration are within-band gains). §24 8→9 rejected (both named levers open; the error summary now reaches 9 of 21 MudForm instances). §27 8→9 rejected a fifth time (denominator grew 49→53; no new i18n substance landed). §31 8→10 rejected (no FinOps evidence since the twenty-seventh cycle; "reversible scale events" still unmet in its exact terms). A FLAG is a held score, not a closure, and none of these ten changed band membership. Update (2026-09-01 twenty-ninth-cycle full re-score, pin v1.179.0, HEAD 65bddd4b): the four-cycle all-rejected run ended. Nine categories were adversarially adjudicated (§4, §5, §7, §12, §21, §22, §24, §25, §27) and 25 returned CONFIRMED on the first pass. Eight of the nine were proposed lifts rejected as verified non-moves: §4 8→9 rejected (the public-setter ground was genuinely fixed in ADC #152, but aggregate-external validation of Event's optional fields and the raw-string OrganizerContactEmail remain). §5 8→9 rejected a fifth time (TD-19 closed, so the map now covers all four modules, but the accepted layered-by-project hybrid is the standing cap and is not a lever). §7 8→9 rejected (byte-identical: seven typed gRPC client registrations, two bidirectional cycles). §12 M3→4/I8→9 rejected (the fan-out lever IS pulled at infra/main.bicep:1596 on the verified TwoReplicaHubFanOutTests.cs:49 proof, but the capacity proof is still out of band AND is now RED, TD-21). §21 M3→4 rejected (SR-pass row still the empty placeholder at adc-ACCESSIBILITY-SCREENREADER-PASS.md:62; axe still E2E-only, chromium-only, non-PR, conditional). §22 8→9 rejected (chromium-only at deploy.yml:691; density options at zero adoption; reflow partial). §24 8→9 rejected (lever (b) closed at 21/21 error summaries, but lever (a), client/server rule mirroring, is open in three places). §27 8→9 rejected a sixth time (DEFERRED, unchanged evidence). The four real moves this cycle were §15 I7→8, §28 I8→9, §33 M3→4 + I8→9 (all CONFIRMED first-pass on new code in PR #162) and §25 I8→7, which is the ninth adjudication: a confirmed down-move on documentation drift, whose proposed maturity 4→3 was rejected because the enforcement leg is intact and kept up with the new pages. Note for the audit trail: the workflow's echoed implementation total of 687/800 used §25's prior 8; the approved and re-summed figure is 685/800. Update (2026-09-04 thirtieth-cycle full re-score): ten adjudications. One confirmed first-pass lift, §4 impl 8→9 (the Email value object inside Event, ADC #177). Two first-pass scores corrected by the verifier and adopted: §9 impl 9→8 (rubric v2 contract-test criterion unmet; the first-pass M3 was rejected) and §16 N/A→M2/I5 (the first pass proposed I6; the verifier held it at the bottom of the Adequate band on two fully open red flags). Seven proposed lifts rejected as verified non-moves at prior: §5 (I9), §7 (I9: no this-cycle change touches the cross-service topology), §12 (I9: TD-21 closed but the only green k6 proof predates the same-day 0.25 vCPU right-size, infra/main.bicep:1267), §23 (I9: code-splitting and virtualization at zero adoption), §25 (M3 and I8 both rejected: adc-NavigationFlow.md still omits all seven routes), §27 (I9, the seventh rejection of the pseudo-localization lever on byte-identical evidence), §31 (I9: no automated surge/revert exists, cost-guard is guard-and-notify only).

✅ 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 Messaging & Integration Architecture · #11 Security · #13 Observability & Operability · #14 Testability & Test Strategy · #15 Best Practices & Code Quality · #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 · #33 Developer Experience & Inner Loop · #34 Architecture Governance & Docs (30 categories at maturity 4; §16 is scored at maturity 2 as of 2026-09-04 and is not on this list) (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 (all live in deploy.needs, deploy.yml:791). 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. UPDATE (2026-07-28 twenty-fourth-cycle re-score): membership and count are unchanged at 30. #15 stays in the protect set for the same reason #18 did: its maturity-4 gate is intact and only its implementation moved (8→7, TD-18 plus two effort-S hygiene items). UPDATE (2026-08-01 twenty-fifth-cycle re-score): membership and count are again unchanged at 30, and no category crossed either threshold; the six adversarial adjudications this cycle were all rejected implementation lifts, so nothing entered or left this list. UPDATE (2026-08-23 twenty-seventh-cycle re-score): membership and count are unchanged at 30. #4 stays in the protect set for the same reason #18 and #15 did: its maturity-4 gate is intact and only its implementation moved (9→8). UPDATE (2026-08-31 twenty-eighth-cycle re-score): membership and count are again unchanged at 30; no category crossed either threshold, and all ten adversarial adjudications were rejected lifts, so nothing entered or left this list. UPDATE (2026-09-01 twenty-ninth-cycle re-score): #33 Developer Experience & Inner Loop JOINED the protect set (scorecard §33 maturity 3→4: the Service Bus emulator parity proof is no longer continue-on-error and is required through cross-service-freshness, TD-17 closed), taking the count to 31: the first addition since 2026-07-17 and the first maturity-band exit since 2026-07-21. #25 stays in the protect set for the same reason #4, #15 and #18 did before it: its maturity-4 gate is intact and only its implementation moved (8→7). This list is the maturity-4 set, not the fully-closed set. Of these 30, 20 also score implementation >= 9, which is the pairing that means "done on both axes"; the other 10 (§5, §7, §9, §15, §18, §23, §24, §25, §27, §31) keep a live row in the implementation band below (§28 left that band on 2026-09-01 at implementation 9). Protect what is here, but do not read presence here as "nothing left to do".)


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:553; 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.

Update 2026-07-28 (twenty-fourth-cycle full re-score, pin v1.131.0, HEAD 2ec77796): one score moved, and it moved down, on the implementation axis only. §15 Best Practices & Code Quality implementation 8→7 (weight 2), the only score move and the only rank change on either band: it becomes the single highest row of the implementation band at implPriority 4, above the four implPriority-3 rows (§7/§18/§21/§28). Maturity holds 4 (independently re-derived from the blanket severity = error baseline, TWAE/AnalysisMode=All, five analyzers, and the required build-and-test Release build with --locked-mode), so #15 stays in the protect set: exactly the two-axis behaviour the bands exist to surface. The basis is hygiene drift plus a scope gap, not a code-quality regression: an audit suppression expired by its own written removal condition (Directory.Build.props:49-51 against its comment at :41-48, with ADR-038 already recording the accepted-advisory list as empty), three undated global NoWarn codes (:22), and the MAUI MMCA.ADC.UI project sitting outside every CI build and outside the CI-audited dependency graph (MMCA.ADC.CI.slnf:25, deploy.yml:288) which is precisely the graph its :8-12 suppressions exist for. The two hygiene items are an effort-S lever on the band row (verify with a full-solution package-mode restore, not CI.slnf; Store carries the identical suppression and should be swept in the same pass); the structural half is new TD-18 (effort L, recorded not scheduled, because a MAUI CI leg cuts against the 2026-07-18 Actions-minute reduction). No closures: closure needs maturity 4 AND implementation >= 9 independently; all four maturity-band items are still M3 and still I8, and no implementation-band category reached 9. The maturity band is byte-identical (§12/§21/§22/§33, #21 at priority 3, #12/#22/#33 at 2, 9 points total). New TD-17 under #33: the Service Bus emulator parity tier is now dispatch-only (cross-service-tests.yml:144) after hanging to its 8-minute timeout on 7 of 7 runs, so #33's header basis is corrected from "nightly plus recency gate" to "no schedule, no gate" and its M3→4 / I8→9 candidacy is re-rejected rather than left pending. TD-16 refreshed and worse (SpeakerDetail.razor.cs is 386, not the recorded 365; HappeningNow still exactly 400 against the cap). TD-15 was not re-verified this run and is left as written. §27 adjudicated DEFERRED after a third rejection on byte-identical evidence, with its cost and re-open triggers recorded in Deliberate / accepted, so it stops returning as a candidacy. Evidence refresh: arch tests 29 classes / 31 files / 91 methods, 91/91 green, of which 90 are now inherited after the §13 pairing gate moved upstream; anchors refreshed repo-wide. Genuinely-open TD set: TD-08, TD-15, TD-16, TD-17, TD-18. Implementation band 15 categories / 35 gap points (was 33). Indices Maturity 97.2% (311/320) / Implementation 85.9%→85.6% (685/800), which is 95.1% of the 90% attainable ceiling, pin v1.131.0, ADR set 001-060.

Update 2026-08-01 (twenty-fifth-cycle full re-score, pin v1.135.0, HEAD 995a7886): no score moved on either axis, so both bands are byte-identical: maturity 4 categories / 9 points (#21 at 3, #12/#22/#33 at 2) and implementation 15 categories / 35 gap points, re-summed this run. No closures (closure needs maturity 4 AND implementation >= 9 independently, and all four maturity-band items are still M3/I8 while nothing on the implementation band reached 9), no new items, no re-ranks. What makes the cycle worth reading is the adjudication pattern: six categories were proposed for an implementation lift and all six were rejected on current source (§5, §13 as a 9→10, §24, §27, §31, §33), each of them exactly one criterion short. Five of those criteria are now named in the band; §24's two levers (client validation does not mirror the server's cross-field and format rules; the error summary reaches 7 of 15 MudForm forms) and §31's (automate the surge and its revert) replace "not yet identified" rows, which is the substantive gain of the cycle. Two items are on repeat rejections and want a decision rather than another pass: §27 (fourth) is already adjudicated DEFERRED, and §33 (second) now rests on the rewritten TD-17. TD-17 half closed: the Service Bus emulator tier is back on the weekday nightly since 2026-07-29 (cross-service-tests.yml:144-146), and its recorded blocker was wrong (the real cause was per-test bus re-provisioning against a ~1 op/sec admin plane, not the companion SQL image); the open half is that it is continue-on-error (:149) and gates nothing. TD-16 re-measured and its headline retired: HappeningNow.razor.cs is 394, not 400, and the high-water mark is now SessionSelectionDashboard.razor.cs at 395, so "flush at the cap" becomes "seven files within 38 lines of the cap". TD-15 was not re-verified for a second consecutive cycle and is left exactly as written. The attainable-ceiling framing is retired here and in the scorecard: a 10 is now awardable for an almost perfect implementation, so the index reads against 100% and the 9-target governs scheduling only. Genuinely-open TD set: TD-08, TD-15, TD-16 (open), TD-17 (half), TD-18. Indices Maturity 97.2% (311/320) / Implementation 85.6% (685/800), both unchanged, pin v1.135.0, ADR set 001-064.

Update 2026-08-14 (twenty-sixth-cycle full re-score, pin v1.152.0, HEAD 19021d93): no score moved on either axis for a second consecutive cycle, so both bands stay byte-identical: maturity 4 categories / 9 points (§21 at 3, §12/§22/§33 at 2) and implementation 15 categories / 35 gap points, re-summed this run. No closures (closure needs maturity 4 AND implementation >= 9 independently, and all four maturity-band items are still M3 while nothing on the implementation band reached 9), no re-ranks. Eight lifts were proposed and all eight were rejected on current source: §5 8→9, §7 8→9, §12 M3→4, §13 9→10, §15 7→8, §23 8→9, §28 8→9 and §31 8→9. The substantive gain of the cycle is that §5's lever is finally named: TD-19, the Notification module's absence from AdcArchitectureMap.cs:12-43 while all three of its projects build in the CI gate (MMCA.ADC.CI.slnf:30-32), effort S, replacing that row's "not yet identified". §7/§16/§21/§22/§25/§28 keep "lever not yet identified, name it at the next re-score". TD-16 re-measured: the high-water code-behind rose 395→398 of the 400 cap, so headroom narrowed from 5 lines to 2, and SessionDetail.razor.cs rose 376→382; still seven files inside a 38-line band, still effort S. TD-17 unchanged in substance, anchors corrected (job :145, needs :146, if :147, timeout-minutes :149, continue-on-error :150, workflow_dispatch :26 + cron '0 6 * * 1-5' :31, gate-keying comment :126-129): the tier stays weekday-nightly, advisory, gating nothing. TD-18 re-confirmed open (its MAUI NoWarn CA5392 anchor drifted MMCA.ADC.UI.csproj:131:143). TD-15 was not re-verified for a third consecutive cycle and is left exactly as written, though its cost-driver topology was re-confirmed (Redis Balanced_B0 infra/main.bicep:864-869, Service Bus Standard :709-714). Anchor and figure refresh (no score move): axe coverage 31 test methods over roughly 29 distinct pages (AccessibilityTests.cs:21-365), not 17 pages; routable @page files 49 (48 excluding the MAUI-only DeviceSettings.razor), not 37, so §27's deferred lift costs roughly 45 pages, not 34; the sev-1 alert infra/main.bicep:481:496-502; the Notification scale pin :1530:1616; §24's error-summary ratio 7-of-15→8 of 18 MudForm-bearing pages (19 forms). Genuinely-open TD set: TD-08, TD-15, TD-16, TD-17 (half), TD-18, TD-19. Indices Maturity 97.2% (311/320) / Implementation 85.6% (685/800), both unchanged, pin v1.152.0, ADR set 001-078.