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.mdtactical register is folded in here (2026-06-26): each deferred sub-item keeps itsTD-NNID and lives under its#NNcategory with its blocker, resolution path, and effort estimate; the recorded-but-not-scheduled choices live in the Deliberate / accepted section below. There is no separate tech-debt file (matching MMCA.Common and MMCA.Store). Effort key: S ≈ hours · M ≈ ~1 day · L ≈ multi-day.
⚠️ Index note (2026-06-27). The 75% / 241-320 figure is the 2026-06-08 single-axis baseline and is not recomputed as items below are ticked: many already-
RESOLVEDrows (#11, #14, #26, #29, #30, #32, …) have moved the real total well past it. For the current, authoritative scores use the canonical, in-repoArchitectureScorecard.md(two-axis, at framework v1.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: thedr-drill.ymlcron is scheduled but gates nothing, so it is Consistent/M3 not an automatic CI gate, the same standard §28 is held to), and the prior "#29 DR-drill gate closed maturity 3→4" claim above is withdrawn; #16 was also reopened (scorecard §16 is maturity 3; deleting the orphan-test folder did not by itself reach 4); §32 moved impl 8→9 (CI restore already runs--locked-modein both gating jobs,deploy.yml:40/:119); and the backlog was caught up to the scorecard by closing #6/#8/#17/#18/#20/#26/#30 (all already at maturity 4). Reconciled 2026-06-30 (enforcement-gate wave): #16/#24/#27/#29/#31 lifted maturity 3→4 by adding CI-enforced governance over already-strong implementation: #24FormsConventionTests, #27TranslationCompletenessTests, and #16FrameworkVersionConsistencyTestsrun in the CI.slnf arch gate (locally verified green, 74/74 arch tests pass); #31 cost-guard and #29 dr-freshness are wired intodeploy.needs(committed, activate on the next push). Reconciled 2026-06-30 (v1.92.0→v1.93.0 sweep, the Common tenth-wave): §5 Vertical Slice Architecture lifted maturity 3→4 (the slice-cohesion fitness functionSliceCohesionTestsis confirmed a CI merge gate inMMCA.ADC.CI.slnf), and §7 was adversarially FLAG-re-checked (a proposed impl 8→9 lift rejected) and confirmed unchanged at M4/I8. Scorecard now Maturity 94.1% / Implementation 85.9% (HEAD89d8439, pin v1.93.0); the §21 a11y axe scans were broadened 10→17 pages (impl 7→8 pending a green nightly), and the recorded screen-reader pass remains the §21 maturity lever. Reconciled 2026-07-02 (re-score, pin v1.99.0): three honest recalibrations, no code regressions. §18 UI Architecture was REOPENED (scorecard §18 maturity 4→3: no automated §18 UI-architecture fitness gate exists, so the container/presentational + code-behind conventions are review-enforced only, making §18 Consistent/M3 not Optimized/M4; its prior maturity-4 "UI convention test" basis was actually the route-authorization tests, a §25 gate). §6 impl was corrected 10→9 (the idempotent inbox covers only 2 of 4 consumer services: Conferenceappsettings.json:32, Identity:29; Engagement/Notification carry none, so real levers remain and 10 was overstated). §27 impl was corrected 8→7 (residual hard-coded English is broader than exception-path only, plus no text-expansion test). Scorecard now Maturity 93.1% (298/320) / Implementation 85.8% (686/800) (pin v1.99.0); the "Scorecard now Maturity 94.1% / Implementation 85.9%" figure above is the frozen v1.93.0 provenance. Reconciled 2026-07-03 (sixteenth-cycle full re-score, pin v1.101.0, HEADac43c8d8, all 34 categories CONFIRMED): the 2026-07-02 e2e-gate promotion is now reflected in this ledger: #28 is CLOSED (scorecard §28 maturity 4: the chromium E2E/axe suite is an enforced deploy gate,deploy.yml:303-308e2e-gatejob +:343indeploy.needs; TD-06 and TD-07 ticked), #21 re-ranked priority 6→3 (scorecard §21 M3/I8 via the same gate; the recorded SR pass remains the cheapest maturity lever), and #19 is REOPENED (scorecard §19 M3/I9: review-enforced conventions, no §19 fitness gate inTests/Architecture/). One implementation recalibration: §24 impl 9→7 (per-form error summary only on the Profile form; the six create forms surface a generic validation snackbar; raw{ex.Message}in Profile snackbars), tracked as new TD-14 under #24 (the category header stays closed: maturity holds 4 onFormsConventionTests). Scorecard now Maturity 94.1% (301/320) / Implementation 85.6% (685/800) (pin v1.101.0); the 93.1%/85.8% figures in this note are the frozen v1.99.0 provenance. Reconciled 2026-07-03 (same-day i18n completion sweep, ADR-027 Decision 9): #27's impl lever CLOSED (scorecard §27 impl 7→8: zero residual literals, dual CI gates incl. the newLocalizedTextConventionTests, MudBlazor chrome + nav localized; a new impl 8→9 sub-item tracks extending the pseudo-loc text-expansion evidence to ADC pages), and TD-14 NARROWED (raw{ex.Message}snackbars eliminated; the Profile-form gate exclusion + per-form error summaries remain). Scorecard now Maturity 94.1% (301/320) / Implementation 85.8% (686/800). Reconciled 2026-07-06 (eighteenth-cycle full re-score, pin v1.106.0, HEAD8fc9e0d2, all 34 categories CONFIRMED): every category re-confirmed at its prior score from evidence read this run (no moves). #8's TD-03 CLOSED: the optimistic-concurrency API round-trip is implemented and deploy-gated (EventDTO.cs:16carries the RowVersion token viaIConcurrencyAware,UpdateEventHandler.cs:34stamps it withSetOriginalRowVersion,OrganizerConcurrencyTests.cs:26asserts a stale token returns 409 inside the deploy-gatingMMCA.ADC.Integration.slnf); scorecard §8 holds impl 9 because the round-trip is Conference-only. #6/TD-02 partially addressed: the genuine broker round-trip test landed as the non-gating nightlyMMCA.ADC.CrossService.IntegrationTests(9 tests, Testcontainers RabbitMQ+SQL), so scorecard §6 holds impl 9; the 9→10 lever is now gating it plus enabling the inbox on all 4 consumer services. Evidence counts refreshed: arch-tests 23 classes / 25 files / 74 methods (all thin subclasses, 0 ADC-local), §14 unit 1507/223 plus integration 303 gating methods / four tiers + 9 non-gating CrossService, coverage floor 38→55.5% (actual ~57%), ADR set 001-038, §27 resx 40 base + 40 es. Scorecard indices hold Maturity 94.1% (301/320) / Implementation 85.8% (686/800). Reconciled 2026-07-10 (nineteenth-cycle full re-score, pin v1.110.0, HEAD246a24dc, all 34 categories held): every category re-confirmed at its prior score from evidence read this run (no moves, no closures, no re-ranks; the below-4 set stays §12/§13/§18/§19/§21/§22/§23/§33 with priorities recomputed byte-identical, and every TD status is unchanged: done TD-01/03/04/05/09/10, open TD-02/06/07/08/13/14). Three first-pass move proposals were adversarially rejected as verified non-moves: §12 impl 8→9 (the Notification app stays pinnedmaxReplicas: 1,infra/main.bicep:1113, even though the v1.110.0 wave provisioned Azure Managed Redis Balanced B0 and scaled the REST services tomaxReplicas: 2), §23 maturity 3→4 (the WebVitals budgets are advisory by design and no §23 fitness gate exists), and §34 impl 9→8 (no governance regression; the untracked workspace-rootArchitecturalAnalysis.mdremains the already-weighed 9-not-10 lever). Evidence refresh: ADR set 001-041, pin v1.110.0, arch tests re-run green this cycle (74/74); a contradictorymain.bicepNotification scale-pin comment (claiming no Redis backplane while the backplane key is injected at:1056) was corrected in place. Scorecard indices hold Maturity 94.1% (301/320) / Implementation 85.8% (686/800). Reconciled 2026-07-15 (twentieth-cycle full re-score, pin v1.116.0, HEAD913d088a, five scores up): the remediation-wave candidacies recorded below were adjudicated. Accepted: #18 CLOSED (scorecard §18 maturity 3→4:UIArchitectureConventionTestsin the CI.slnf arch gate), #19 CLOSED (scorecard §19 maturity 3→4:StateManagementConventionTestsin the same gate, impl held at 9 after a first-pass 9→8 proposal was adversarially rejected as unsupported), #23 CLOSED for maturity (scorecard §23 maturity 3→4: the CWV budgets became enforced assertions inside the deploy-gating chromiume2e-gateon 2026-07-11, superseding the nineteenth-cycle advisory-by-design rejection; the WASM code-split/image sub-item stays open as impl polish), #13's impl half (scorecard §13 impl 8→9 on the SLO workbook +infra/OPERATIONS.mdday-2 runbooks), and TD-14 confirmed (scorecard §24 impl 7→8). Rejected, headers corrected below: the #13 maturity 3→4 candidacy (runbooks/dashboards are review-enforced conventions and IaC, not CI-gated fitness functions, so §13 holds M3/I9 and REOPENS), the #22 maturity 3→4 candidacy (the firefox/webkit legs added to the e2e-gate runcontinue-on-error: truepere2e.yml:74, i.e. advisory inside the gate, so §22 holds M3/I8 and REOPENS), and the #33 impl 8→9 candidacy (broker parity local-RabbitMQ vs prod-Service-Bus is mitigated, not closed, perREADME.md:74, a live rubric red flag, so §33 holds M3/I8 and REOPENS). Also corrected in the scorecard: §28's false "E2E #5 un-skipped" claim (the test is re-quarantined atSpeakerSelfServiceTests.cs:57; score held M4/I8) and the §34 impl 9→8 downgrade re-rejected. Scorecard indices move to Maturity 96.6% (309/320) / Implementation 86.3% (690/800); the below-4 set narrows to §12/§13/§21/§22/§33. Reconciled 2026-07-17 (twenty-first-cycle full re-score, pin v1.117.0, HEADc4c01aa5, two scores up): the two 2026-07-16 gate candidacies were adjudicated ACCEPTED. #13 CLOSED (scorecard §13 maturity 3→4:ObservabilityConventionTestsmachine-enforces the alert-to-runbook pairing in the CI.slnf arch gate,MMCA.ADC.CI.slnf:56+deploy.yml:57,417; impl holds 9) and #22 CLOSED (scorecard §22 maturity 3→4: the deploy-gatinge2e-gatepasses all three engines,deploy.yml:309, ande2e.yml:78scopescontinue-on-errorto scheduled nightly non-chromium legs, so every invoked engine can fail a deploy; impl holds 8). Rejected: the #27 impl 8→9 pseudo-loc candidacy (PseudoLocalizationTests.cs:51covers 3 public pages of 30+, a partial extension; §27 holds M4/I8 as a verified non-move). Corrected: a stale nineteenth-cycle draft accidentally committed via PR #15 (2026-07-17) had relabeled the #12 header "RESOLVED M4/I8" and added a mislabeled "2026-07-12 twentieth-cycle" update paragraph; both are reverted below, and §12 stays M3/I8 open per the twentieth-cycle adjudication (re-confirmed this run: the k6 tier is freshness-gated viaload-freshness,deploy.yml:348,417, but executes monthly/dispatch out of band, and Notification stays pinnedmaxReplicas: 1). #33 re-confirmed M3/I8 (the 2026-07-16 Service Bus emulator tier candidacy stands recorded for a future cycle; the tier is nightly, riding the freshness gate rather than in-band). Scorecard indices move to Maturity 97.8% (313/320) / Implementation 86.3% (690/800); the below-4 set narrows to §12/§21/§33. Reconciled 2026-07-21 (twenty-second-cycle full re-score, pin v1.121.0, HEAD8509a05d, two scores down, neither a quality regression): #22 REOPENED (scorecard §22 maturity 4→3: the 2026-07-18 Actions-minute reduction cut the deploye2e-gatetobrowsers: '["chromium"]',deploy.yml:488with its rationale comment at:478-480, so firefox/webkit run only on the weeknight nightlyschedulewheree2e.yml:119keeps themcontinue-on-error; cross-engine verification is nightly-advisory again, which is M3, and the trade-off is recorded in Deliberate / accepted with its scoring cost stated plainly). §18 implementation 9→8 (the category header stays closed, maturity holds 4 on theUIArchitectureConventionTestsgate, but the largest code-behind sits flush at the enforced 400-line cap with zero headroom,HappeningNow.razor.cs:400vsUIArchitectureConventionTestsBase.cs:22, plus six files in the 360-379 band; tracked as new TD-16 under #18, effort S). Rejected for a second consecutive cycle: the #27 impl 8→9 pseudo-loc candidacy (PseudoLocalizationTests.cs:51still covers exactly 3 public pages of 36 routable pages, unchanged since the twenty-first-cycle rejection; §27 holds M4/I8). Sub-item closed: #12's deferred prod-Redis provisioning (Redis Enterprise is provisioned,infra/main.bicep:740,753,771), though the SignalR fan-out stays unexercised behind themaxReplicas: 1pin (:1424), so #12 itself stays open. Corrected: #33's load-bearingREADME.md:74quote no longer exists (the file now states the opposite atREADME.md:80-84, the Service Bus emulator tier having landed), and drifted anchors were refreshed repo-wide (CI.slnf:56→:58,deploy.yml:303-309/343/348/417→:483-489/:553/:783,e2e.yml:78→:119,main.bicep:1113→:1424,:341→:488). Scorecard indices move to Maturity 97.2% (311/320) / Implementation 85.9% (687/800); the below-4 set widens to §12/§21/§22/§33. Reconciled 2026-07-23 (twenty-third-cycle full re-score, pin v1.123.0, HEAD160f59f5, no moves): every category re-confirmed at its prior score from evidence read this run (no closures, no new items, no re-ranks, no TD changes; the below-4 set stays §12/§21/§22/§33 with priorities unchanged: #21 at 3, #12/#22/#33 at 2). Two first-pass maturity-lift proposals were adversarially rejected as verified non-moves: §12 M3→4 rejected (the k6 tier still runs monthly/dispatch out of band,load-test.yml:8, withload-freshnessa recency-only deploy check,deploy.yml:553, and Notification pinnedmaxReplicas: 1,infra/main.bicep:1424) and §21 M3→4 rejected (the recorded manual screen-reader pass is still the empty placeholder inACCESSIBILITY-SCREENREADER-PASS.md, remaining the cheapest maturity lever). The v1.122.0/v1.123.0 lockstep sweeps moved no score. Scorecard indices hold Maturity 97.2% (311/320) / Implementation 85.9% (687/800), ADR set 001-051. 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 carryEnableInbox=truesince 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 indeploy.needssince 2026-06-30), the scorecard's §8/§28 "re-quarantined" claim (E2E #5 was un-quarantined 2026-07-19, plain[Fact]atSpeakerSelfServiceTests.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 asadc-ACCESSIBILITY-SCREENREADER-PASS.mdin Websitedocs-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, HEAD2ec77796, 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-51vs its comment at:41-48), three undated globalNoWarncodes (:22), and the MAUIMMCA.ADC.UIproject 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-12suppressions 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.csis now exactly 400 lines against the enforced cap, and the recorded 360-379 band was stale (SpeakerDetail.razor.csis 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.csfiles / 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:7is 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.ymle2e-gate:488→job at:500withbrowsers: '["chromium"]'at:505and 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.ymlemulator job at:142with 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, HEAD995a7886, 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, andAdcArchitectureMap.cs:12-44omitsMMCA.ADC.Notification.Applicationfrom 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 atinfra/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: theservicebus-emulator-smokejob is back on the weekday nightly since 2026-07-29 (cross-service-tests.yml:144-146needs: should-run+if: needs.should-run.outputs.run == 'true'undercron: '0 6 * * 1-5'at:26,:30,timeout-minutes: 10at:148), and the recorded root cause was wrong: the comment at:130-143records 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 iscontinue-on-error: true(:149) and gates nothing, sincecross-service-freshnesskeys off thecross-servicejob (:124-128, gate atdeploy.yml:663). TD-16 re-measured, and the headline is no longer true:HappeningNow.razor.csis 394, not 400, and the high-water mark moved toSessionSelectionDashboard.razor.csat 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 HEAD995a7886, 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 drifteddeploy.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.ymle2e-gate:500→:531withbrowsers: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-moderestores at:199/:299,e2e.yml:131→:144with 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.propssuppression:49-51→:54and NoWarn:22→:26, ADC pin:123→:139at 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, HEAD19021d93, 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, andAdcArchitectureMap.cs:12-43still has noModule("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 touchedload-test.yml,deploy.ymlorTests/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 movesinfra/main.bicep:481→:496-502,severity: 1at: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, scheduleworkflow_dispatch:26+ cron'0 6 * * 1-5':31(the recorded:26,:30was 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@pagefiles underSource(48 excluding the MAUI-onlyDeviceSettings.razor), not 37, so §27's deferred lift now costs roughly 45 pages; the Notification scale pin movesinfra/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 MAUINoWarn CA5392anchor movesMMCA.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, HEAD96f0919a, two scores down): §4 Domain-Driven Design implementation 9→8 (weight 3; public-setter cross-aggregate navigations onSession/Sponsor/Activity, aggregate-external validation ofEvent's newer optional fields against the repo's own Sponsor convention, andEvent.OrganizerContactEmailas a raw string where theUser/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 atadc-ACCESSIBILITY-SCREENREADER-PASS.md:62, #12 still scale-pinned atinfra/main.bicep:1648with its rationale at:1643-1647, #22 still chromium-only atdeploy.yml:541, #33's parity tier still advisory atcross-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-gateruns only when thechangesjob marks the diff UI-affecting,deploy.yml:538with rationale:533-537, and thedeployjob accepts a skipped gate,:896with 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: theintegration-testsjob is PR-only (if: github.event_name == 'pull_request',deploy.yml:389) and is NOT indeploy.needs(:866), so the "gates every deploy" / "deploy-gatingMMCA.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.cs367→398,ADCHome.razor.cs341→380,EventDetail.razor.cs365→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 atDirectory.Packages.props:139" is doubly stale, now v1.160.0 atDirectory.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, HEADb04b3a3e, 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 atadc-ACCESSIBILITY-SCREENREADER-PASS.md:62; #12 still pinnedmaxReplicas: 1atinfra/main.bicep:1591with the right-sizing rationale ending:1589, the recorded:1648having drifted back; #22 still chromium-only atdeploy.yml:638; #33's parity tier stillcontinue-on-error: trueatcross-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 UIIUiReadCacheopt-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 nowSessionDetail.razor.csat exactly 400 of the 400 cap (zero headroom; cap confirmed unoverridden atUIArchitectureConventionTestsBase.cs:22with no subclass override inUIArchitectureConventionTests.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 gatedeploy.yml:663→:760; TD-18's CI.slnf-scoped vulnerable scandeploy.yml:319/:328→:416/:425; TD-19's:12-43map anchor still accurate; TD-20's full anchor set:538/:533-537/:896/:880-883→:635/:630-634/:1022/:1006-1009with the job at:628and 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.razorfiles, resx pairs 53+53→68+68, §24 error summaries →9 of 21MudForminstances (20 files), lock files 66→67, routable@pagedenominator 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 atDirectory.Packages.props:100-123(16 lockstep packages). Reconciled 2026-09-01 (twenty-ninth-cycle full re-score, pin v1.179.0, HEAD65bddd4b, four score moves): #33 is CLOSED on both axes (scorecard §33 maturity 3→4 and implementation 8→9):servicebus-emulator-smokecarries nocontinue-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-freshnessnow requires both broker jobs to have concluded success (deploy.yml:874, deploy fails otherwise:885, indeploy.needs:1054, required:1089), andADC_BROKER=servicebusgives 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.htmlbaselines, missing baseline reported as a non-match, all three projects inMMCA.ADC.CI.slnf:43,:49,:55) plus the newbackend-test-gate(deploy.yml:394-396,:1054,:1093). #15 rises to implementation 8 with both effort-S hygiene levers struck:Directory.Build.propsnow carries zeroNuGetAuditSuppressitems and no GHSA id (the expired SQLite entry and theSystem.Private.UriMAUI entries are gone; the live high advisory is remediated by a patched SSH.NET 2026.0.0 pin atDirectory.Packages.props:83), and the global NoWarn line isCS1591;EXTEXP0001;S8970at:31with each code dated and justified (:16-21,:22-26,:27-30) and RMG020 scoped to.Applicationprojects (: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,/engageorspeaker/qr, leaving 7 of the 53 routable@pagefiles undocumented and the authorization enumeration at:532-534incomplete, 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'sNavigationContractTestsas an optional second (effort M). TD-19 is CLOSED (AdcArchitectureMap.cs:51-54now 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 isPublicSessionDetail.razor.csat 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 newbackend-test-gate(deploy.yml:394-396, indeploy.needs:1054, required:1093), which carries the exact complement ofe2e-gate's condition so no code deploy runs with zero test execution; the open half is unchanged,e2e-gateis still ui-scoped (:688, job:677) anddeploystill 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 fromMMCA.ADC.CI.slnf(:25-26),NoWarn CA5392still ungated (MMCA.ADC.UI.csproj:151), the vulnerable-package scan stillCI.slnf-scoped (deploy.yml:465), and the newmaui-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_failedagainst the gateway per-client-IP edge limiter, and the 35-dayload-freshnessrecency 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 atadc-ACCESSIBILITY-SCREENREADER-PASS.md:62; §22 still chromium-only atdeploy.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@pagefiles 53, cross-service integration tests 9→10 (the newTwoReplicaHubFanOutTests.cs:49cross-replica SignalR proof, green in nightly run 33500459363), NotificationmaxReplicas1→2 (infra/main.bicep:1596, rationale:1586-1595, and nomaxReplicas: 1remains 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 atDirectory.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 tier→ DONE (#14): per-serviceWebApplicationFactorytiers, ~345 tests gating every deploy, also closed #11's authz-gate and #16's non-building projects, and advanced #8. SeeIntegrationTestReworkPlan.md.- Integration-coverage expansion (2026-07-06): ~74 new integration tests closed the endpoint gaps the rework left open (OAuth challenge/exchange, JWKS + OIDC discovery, DecisionSupport session-selection, Sessionize refresh, output-cache eviction, audit-stamp fidelity, RFC 9457 contract shape, GDPR export, preferences) plus explicit
[Idempotent]on Events/Sessions create; the three per-service fixtures were consolidated ontoSqlServerIntegrationTestFixtureBase. A new Notification integration project (SignalR hub + inbox) closed the last untested service. The deferred Phase 4 broker-transport tier landed asMMCA.ADC.CrossService.IntegrationTests(Testcontainers RabbitMQ + SQL, non-gating nightlycross-service-tests.yml). Deliberately skipped: dedicated rate-limit fixtures (the WAFs neutralize the limiter; proving the 300/min cap + per-IP registration throttle needs a tight-limit fixture variant, low value for the volume, revisit only if abuse is observed).→ DONE: fixed #19 (6 create forms) and #24 (6 inline-edit paths); both categories are now closed at maturity 4.UnsavedChangesGuardsweep- bUnit + axe-core harness → lifted #28 and #18 (both closed); #21 remains (the recorded screen-reader pass is its open lever).
- Doc/CLAUDE.md drift fixes → close confirmed flags in #9, #34 (and the #7 note).
- Credential hardening is one throughline across #26, #11, #17.
⚠️ Severity vs. scale. Several operational risks (#29, #12, #31) were severity-adjusted down in the audit because real conference-day load is ~76 accounts / ~67 peak concurrent. Right-size the fixes: don't over-engineer DR/scale for that volume.
🔴 Priority 6: highest leverage
[x] #26 · Front-End Security · 2 → 4 (weight 3) · RESOLVED 2026-06-29 (scorecard §26 maturity 4 / impl 9); only the deferred TD-08 data-call proxy remains
Token handling uses two rubric-named anti-patterns, with no CSP defense-in-depth. Status (2026-06-27): cookie-only refresh + in-memory access (auth-path BFF), OAuth code-exchange, enforced CSP + hardened headers on both UI host and Gateway (now regression-gated by SecurityHeadersTests), and the 7-day refresh cookie with a recorded SameSite=Lax decision are all done. The only open piece is the deferred TD-08 full same-origin data-call proxy (access token also out of JS) + the login/register/OAuth proxy: needs interactive Aspire verification + release.
(High) JWT access AND refresh tokens persisted in JS-readableIMPLEMENTED 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):localStoragelocalStorageis gone; the refresh token lives only in the HttpOnly cookie and is exchanged server-side (/auth/session/token+UseCookieSessionRefresh+ICookieSessionRefresherin MMCA.Common.API), and the access token is held in memory (short-lived), hydrated from the cookie via the same-origin proxy (SameOriginProxyTokenRefresher). Residual: the refresh token transits JS only during the login round-trip (to seed the cookie); the login/register/OAuth proxy that closes even that window is deferred. SeeTokenStorageDesignNote.md.(High) OAuth completion redirect carries both tokens in the URL query stringRESOLVED (Wave 1, item ①):OAuthController.CompleteAsyncnow mints a single-use code, stashes the token pair in the cache, and redirects with only?code=…; the UI redeems it viaPOST auth/oauth/exchange(OAuthController.ExchangeAsync). Tokens no longer touch the URL, history,Referer, or access logs.- (Medium)
No CSP or security headersRESOLVED (UI host):SecurityHeadersMiddlewaresets nosniff /X-Frame-Options: DENY/ Referrer-Policy / Permissions-Policy, plus a full CSP now enforced withconnect-srcpinned to the Gateway origin (https + wss): falls back to Report-Only only if the endpoint can't be resolved. Gateway headers now set too (2026-06-14):GatewaySecurityHeadersMiddlewareadds nosniff / X-Frame-Options / Referrer-Policy / Permissions-Policy / CSPframe-ancestors 'none'+ HSTS (prod) on every Gateway response (TD-09: done 2026-06-14, effort S).
Fix
- [~] Move to an HttpOnly-cookie-only or BFF/token-handler model so tokens are never JS-readable. → implemented as the auth-path BFF (C+ proper): cookie-only refresh + in-memory access. Full data-call proxy (access also out of JS) deferred; login/register/OAuth proxy (closes the login-flash) deferred. 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 byOAuthControllerTests(success, replay-burn, missing/expired, empty-code). - Add a CSP + standard security headers on the UI host and the Gateway. → DONE (both): UI host CSP enforced with
connect-srcpinned (BlazorCspPolicyProvider); the Gateway sets the hardened headers on every response via the sharedAddCommonSecurityHeaders/UseCommonSecurityHeadersmiddleware registered first in its pipeline (Source/Hosts/MMCA.ADC.Gateway/Program.cs:31,61). (The line-31 audit note above mentioned a bespokeGatewaySecurityHeadersMiddleware; the shipped implementation is the shared Common middleware: same headers, one source.) - Add an integration/E2E test asserting header presence so it can't regress. → DONE (2026-06-27):
MMCA.ADC.Gateway.Tests/SecurityHeadersTestsboots the real Gateway viaWebApplicationFactory<Program>(no SQL, runs in the fast CI tier /CI.slnf) and asserts/alivecarriesX-Content-Type-Options: nosniff,X-Frame-Options: DENY,Referrer-Policy,Permissions-Policy, CSPframe-ancestors 'none', and HSTS (Production env). A refactor droppingUseCommonSecurityHeaders()now fails CI. - Shorten the 30-day refresh cookie; consider
SameSite=Strict. → DONE (2026-06-27, with recordedSameSitedecision): the session/refresh cookie is already 7 days (not 30):SessionCookieJar(MMCA.Common.API) pinsLifetime = TimeSpan.FromDays(7), "aligned to the refresh-token lifetime so a cookie never outlives the credential it carries."SameSite=Strictis deliberately NOT adopted:SameSite=Laxis load-bearing for the SSR-prerender path ([Authorize]pages opened in a new tab / on F5 / following an external link are cross-site top-level navigations that Strict would strip the cookie from, forcing a spurious /login bounce, the exact scenario ADR-022's cookie scheme exists to serve); CSRF is covered defense-in-depth by the/auth/session/tokenendpoint'sSec-Fetch-Sitecheck + POST-only +SameSite=Lax.
[x] #28 · Front-End Testing & Quality · 3 → 4 (weight 3) · RESOLVED 2026-07-02, reconciled here 2026-07-03 (scorecard §28 maturity 4 / impl 8): the chromium E2E/axe suite is an enforced deploy gate (e2e-gate in deploy.needs, deploy.yml:303-308,:343; e2e.yml:31 workflow_call), closing TD-06 and TD-07. Firefox/webkit stay advisory nightly (#22); visual-regression snapshots remain optional polish. 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-48runs onlyCI.slnf; E2E needs the full Aspire stack and is run manually, so UI regressions can merge to prod undetected. - (Medium) Accessibility untested: no axe/Lighthouse anywhere.
- (Low) No bUnit/component tests for ~45 Blazor components.
Fix
- Add a bUnit component-test project (conditional rendering / edge states). → DONE (3 module projects):
MMCA.ADC.Conference.UI.Tests(bUnit v2 harness, MudServices + loose JSInterop + permissive-auth doubles so<AuthorizeView>renders), inCI.slnf, covering the three public detail pages (Event/Speaker/Session: loaded vs not-found) plus the Session page's<AuthorizeView>action bar (hidden anonymous / shown authenticated);Identity.UI.Tests(a mutable-auth harness, since Identity pages injectAuthenticationStateProviderdirectly):Profileloaded/error-state bUnit tests + the/usersauthz fitness test; andEngagement.UI.Testscovering both feedback forms:EventFeedbackTests(dynamic question render by type + per-question upsert skipping unanswered) and nowSessionFeedbackTests(2026-06-27): precondition gating (BR-16 unscheduled / BR-91 service / BR-49 status block the form), session-not-found error state, question render by type, and upsert-only-answered. List pages deliberately skipped for bUnit:DataGridListPageBaseis infra-heavy (7 injected services + JS interop/PersistentComponentState); its plumbing belongs to MMCA.Common's own tests, the derived page logic is thin. - Add a route-authorization fitness test,
ManagementRouteAuthorizationTests(reflection over Conference.UI): admin-namespace pages must keep[Authorize(Roles="Organizer")], the set is asserted non-empty (no vacuous pass), and public pages must stay anonymous at the page level. Closes the #25 residual. - Wire axe-core (
Deque.AxeCore.Playwright) + ≥1 a11y assertion (TD-06) → DONE (2026-07-02, ticked on the 2026-07-03 reconciliation): the axe-coreAccessibilityTests(17 pages,Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/AccessibilityTests.cs) run inside the deploy-gating chromiume2e-gatejob (e2e.yml:236runs the whole E2E project;deploy.yml:343puts e2e-gate indeploy.needs), so the a11y assertions 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-gatejob (deploy.yml:303-308uses: ./.github/workflows/e2e.ymlwithbrowsers='["chromium"]';e2e.yml:31workflow_call), promoted after validation run 28604877733 (first fully green three-browser matrix). The former Blazor-Server-under-load blocker was resolved by theE2E_FORCE_SERVERpin + reload-and-rewait fixes (see the 2026-07-02 notes below). - [~] Add Playwright visual-regression snapshots for key pages. → 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.htmlbaselines, OS-independent, missing baseline = non-match), gating every PR viaMMCA.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:ForceWebAssembly → App.razor), validated on a fast local box (it even surfaced and fixed real per-test issues: the RenameCategory persisted-filter bug, the speaker-dashboard stale cache). Its first CI execution (run 28560329396, 2026-07-02; the two intervening nightlies never reached the tests: a GitHub Actions billing lapse on 06-30 and the Microsoft.OpenApi NU1903 advisory on 07-01, both since resolved) failed wholesale: 0 passed / 24 uniform ~110s timeouts in 44 min, job killed at the 50-min cap. Evidence from the run: prerendered pages render fine (web-vitals JSONs captured, LCP ≈ 200-400ms), the backend is healthy (warm-up POST /Auth/login → 200), but no interactive flow ever completes: no navigation, no logout button, and no error alert either: clicks land on a dead prerendered DOM. On the 2-core hosted runner every fresh browser context pays a cold WASM runtime boot while the whole stack shares the same cores, and WaitForBlazorAsync's readiness probe (window.Blazor?._internal) is satisfied during prerender, so the suite interacts before WASM interactivity exists. Same outcome as the 2026-06-20 v1.72.0 attempt above ("WASM-mode auth fails in the CI E2E env… stalling the suite into the 50-min job cap"), now with the mechanism identified. Decision: e2e.yml no longer sets E2E_FORCE_WASM (back to the InteractiveAuto 84/93 Server-mode baseline); the BR-213 registration-throttle lift is preserved via a new independent E2E_LIFT_REGISTRATION_THROTTLE AppHost gate; per-failed-test Playwright traces now ride the CI artifact (E2E_TRACE=artifacts/traces/) so any future red nightly is diagnosable offline (this run had no traces; the env var was never set in CI). E2E_FORCE_WASM remains supported for local fast-box runs, where WASM mode works. If anyone retries WASM in CI, it needs all three of: (a) a WASM-aware readiness signal (a marker rendered only by interactive code, not Blazor._internal), (b) amortizing the per-context WASM boot (fresh Playwright contexts have no shared cache, e.g. serve the _framework bundle from a shared route-cache), and (c) a raised job cap; any one alone repeats this failure.
Residual-9 trace triage (2026-07-02, run 28589825631: 89/99, the first run with per-failed-test traces) and the InteractiveAuto discovery. All eight timeout failures share ONE frame: the post-login WaitForBlazorAsync inside E2ETestBase.LoginAsync, and seven of eight are LoginAsUserAsync (the attendee cluster, late-suite). The traces overturn the "pure Server-mode circuit drop" reading for this cluster: the network capture shows a _framework/*.wasm download storm mid-test (520 requests in one trace) because InteractiveAuto switches each test's SECOND page load (the post-login forceLoad of "/") to the background-downloaded WASM bundle, whose .NET runtime boot under 2-core contention exceeds every wait; the bundle download itself also starves the live Server circuits (the login click's 60s three-way auth-wait timeout). A second latent bug: Playwright's timeout exception derives from System.TimeoutException, NOT PlaywrightException, so LoginAsync's catch-and-rewait never actually caught it (the built-in "retry" never ran). The ninth failure (Speaker_EditOwnProfile, BR-207) burned its 8 re-login attempts on the same contended UI login path instead of measuring event propagation. FIXES (2026-07-02): e2e.yml pins E2E_FORCE_SERVER=true (App.razor three-way mode: CI pins Server, E2E_FORCE_WASM stays local-optional, prod stays InteractiveAuto); Common E2ETestBase post-auth wait now catches both exception types and RELOADS once before re-waiting (fresh request, HTTP-cached assets) instead of watching the same stalled boot; LoginAsLinkedSpeakerAsync polls POST /Auth/login via the API for the speaker_id claim and performs a single UI login only after propagation lands. Target: chromium at or above 97/99 over a 3-nightly soak, then promote chromium E2E to a merge gate (the standing #28 exit criterion). Full plan: workspace Docs/Planning/E2E-RemainingFlakes-plan.md.
MERGE-GATE PROMOTED (2026-07-02, user-directed ahead of the soak). Validation run 28604877733 on the full fix stack returned the first fully green three-browser matrix ever (chromium 99 tests / 0 failed / 1 retry; firefox and webkit green outright), and the gate was promoted immediately: e2e.yml gained a workflow_call entry point with a browsers input (dispatch/nightly keep the full matrix), and deploy.yml now has an e2e-gate job (uses: ./.github/workflows/e2e.yml with browsers='["chromium"]') in deploy.needs alongside cost-guard and dr-freshness. A red chromium suite now blocks the production deploy; firefox/webkit stay advisory on the nightly. Deploy latency cost: one chromium E2E job (roughly 40 minutes) per deploy. The 3-nightly soak still runs as confirmation; if a genuine contention flake blocks a deploy, re-run the job after reading its trace artifact, do not demote the gate on a single red. This is the #28 maturity 3-to-4 lever (E2E is now an enforced deploy gate, not nightly-only); the next re-score should re-evaluate #28 and the #22 cross-browser item (green firefox/webkit matrix).
[x] #29 · Resilience, Reliability & Business Continuity · 3 → 4 (weight 3) · RESOLVED 2026-06-30 (scorecard §29 maturity 4 / impl 9): a dr-freshness job in deploy.needs now gates the deploy on a recent successful DR drill, so the recovery proof is enforced by a CI gate (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 (serviceDatabaseLtrinmain.bicep); PITR is already geo-redundant (Basic default). Restore drill automated (one-clickdr-drill.yml+scripts/dr-restore-drill.ps1) and executed end-to-end 2026-06-20: PITR restore ofADC_Conferenceinto a throwaway copy in 2.6 min (vs 2 h RTO), verified Online, cleaned up; row recorded inDISASTER-RECOVERY.md. §29 residuals (TD-10, effort S), DONE 2026-06-20: the fault-injection test (ResilienceCircuitBreakerFaultInjectionTests+ outbox broker-degrade, in MMCA.Common), the automated/executed restore drill, and the Azure Monitor SLO workbook (sloWorkbookinmain.bicep←workbooks/adc-slo-workbook.json) all landed. - Make the restore drill an actual merge/deploy gate (maturity-4 lever) → DONE 2026-06-30: a lightweight
dr-freshnessjob was added todeploy.ymland to thedeployjob'sneeds. It fails the deploy unless the latestdr-drill.ymlrun concludedsuccesswithin an 8-day freshness window (covering the weeklycron: '0 6 * * 1'), via onegh apiActions read, so the recovery proof now blocks the deploy with no per-deploy restore cost (right-sized for the ~67-peak load). The real PITR restore still runs ondr-drill.yml's weekly cron;GracefulShutdownTestsremains CI-gated, so impl holds at 9. Live indeploy.needssince 2026-06-30 (phrasing refreshed 2026-07-23). Effort S. - Add metric + log-query alerts with an action group for key SLOs:
main.bicepnow provisions an action group + three App-Insights metric alerts (failed requests, server response time, dependency failures), email via theALERT_EMAILrepo variable. -
ConsiderDeliberately deferred: 2026 load (~76 acct) didn't warrant it; recorded as accepted risk in DISASTER-RECOVERY.md.minReplicas:2for the gateway/UI on conference day only. - Add a basic fault-injection / graceful-shutdown test. → DONE (2026-06-27): the fault-injection half was already covered by
ResilienceCircuitBreakerFaultInjectionTests+ the outbox broker-degrade test in MMCA.Common (TD-10). The graceful-shutdown half now lands asMMCA.ADC.Gateway.Tests/GracefulShutdownTests: it boots the real Gateway host viaWebApplicationFactory<Program>, requests a stop under a bounded 20s token, and assertsIHost.StopAsyncdrains and completes (the host reachesApplicationStopping→ApplicationStopped) within the timeout; a hosted service that refused to drain would cancel the token and fail the test. Headless, inCI.slnf.
🟠 Priority 4
[ ] #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 atDependencyInjection.cs:33with theanthropic-versionheader 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 substitutesFakeAiScoringService; 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 thatdeployneeds. 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 onlyModelId(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 aPromptVersionconstant persisted next toModelId, and emit input/output token counters tagged by model and prompt version with a monthly-spend alert ininfra/. 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:RESOLVED:User.Delete()retains email, name, password hash/salt, device metadata, OAuth keys indefinitely.Usernow implements the frameworkIAnonymizableextension point (v1.53.0);User.Anonymize()irreversibly overwrites email (→ uniquedeleted-{id}@anonymized.invalid), name, password hash/salt, device metadata, OAuth keys, and revokes the refresh token, idempotently, keeping the row for FK/audit (anonymize-in-place, ADR-005).DeleteUserHandlercalls it on every deletion request, so erasure is immediate: well inside thePRIVACY.md§5 "30 days" promise. Covered byUserAnonymizeTests(3 domain tests);DeleteUserHandlerTestsgreen.(Medium) PII (email + first/last name) written to App Insights traces with no redaction.RESOLVED: the four PII-bearingUserRegisteredHandler.cs:179-198.LoggerMessagetemplates (email ×3, name ×1) now log only the stable{UserId}/counts, no email or name reaches the trace pipeline (matchesPRIVACY.md§1.2's stated log scope).(Medium) Data-subject access/export is manual-email-only; only deletion has an endpoint.RESOLVED (Identity-owned data):GET /users/{userId}/export(owner or Organizer) returns a portableUserDataExportDTO(email, name, role, login provider, device metadata, speaker link, timestamps), excluding credentials (hash/salt, refresh token, provider key). Covered byExportUserDataHandlerTests. Cross-service aggregation (Engagement bookmarks, Notification messages) for full §7 coverage remains.
Fix
- Implement a real erasure path:
IAnonymizable+ anonymize-on-delete (immediate erasure). (A scheduled-purge backstop for rows soft-deleted by other paths is optional now that delete erases inline.) - Redact/tokenize PII before logging: done in
UserRegisteredHandler. - Add an export/access endpoint:
GET /users/{userId}/export(Identity-owned data);cross-service bookmark/notification aggregation is the remaining piece→ cross-service aggregation DONE 2026-07-11 (remediation wave 6): the export now aggregates Engagement (session bookmarks + submitted live-Q&A questions, newuser_engagement_export.protorpc mirroring the bookmark-count pattern) and Notification (inbox items, newuser_notification_export.protorpc on the existing ADR-012 grpc ingress; a newNotification.Sharedlayer carries the 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 (AppHostWithReferencewithout deadlockingWaitFor; bicep env mirroring the existing gRPC-edge mechanism). 9 handler unit tests + a payload-shape integration test (faked peers). Recorded follow-up, deliberately out of scope: event/session feedback answers live in the Conference DB (EventQuestionAnswer/SessionQuestionAnswer), so full-corpus export would need a third (Conference) edge; the recorded §30 residual named only bookmarks + notifications, both now covered. §30 Implementation 9→10 candidacy recorded for the next re-score. - Add a fitness/integration test proving an erasure path exists and that PII is not logged: domain unit tests added; the end-to-end erasure + no-PII-in-logs assertion rides the #14 integration-tier rework. SHIPPED 2026-07-16:
ErasureAndPiiLoggingTests(Identity integration tier, gating every PR as a required check; wording corrected 2026-08-23, theintegration-testsjob is PR-only,deploy.yml:389, not indeploy.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 newPiiLogCapturesink in the test factory, asserted against unique markers). §30 I9→10 candidacy already recorded stands on stronger evidence.
[x] #27 · Internationalization · 3 → 4 (weight 1) · RESOLVED 2026-06-30, scorecard §27 maturity 4 / impl 8 as of the 2026-07-03 i18n completion sweep: dual CI gates (TranslationCompletenessTests floor 40 + the new LocalizedTextConventionTests), zero residual hard-coded literals (titles/snackbars/breadcrumbs/nav/home), MudBlazor chrome localized via the inherited ResxMudLocalizer, ErrorMessages.Success concatenation eliminated (obsoleted upstream, 28 sites swept). The impl 8→9 lever is extending the pseudo-loc no-overflow (text-expansion) E2E evidence, which today covers only the shared chrome in Common's gallery gate, to ADC's own pages
(Low) Hardcoded user-facing English throughout markup, e.g.RESOLVED (v1.86.0 sweep, 2026-06-27): ADC now ships real en-US + es i18n (36 baseSource/Modules/Conference/.../Pages/Speaker/SpeakerDashboard.razor:7-60; no.resx, noIStringLocalizer,InvariantCulturedisplay..resx+ 35.es.resxacross the three module UIs + three API error-resource sets,IStringLocalizer<T>in ~33 pages, culture-aligned SSR/Server/WASM, cross-deviceUser.PreferredCulturepersistence, backend error localization keyed onError.Code,SupportedCultures = [en-US, es]). The scorecard flips §27 from N/A to scored at Maturity 3 / Implementation 8. ADR-011 (single-locale) is superseded by ADR-027.
Fix (weight 1)
-
Cheapest: record an ADR/note that single-locale is intentional.→ SUPERSEDED: ADR-011 is now superseded by ADR-027 (multi-locale i18n, canonical in MMCA.Common) (en-US + es); the prior single-locale stance no longer holds. - Externalize strings to resources + register
AddLocalization/RequestLocalization+ culture-aware date formatting: done on the v1.86.0 sweep (evidence above); was formerly the conditional "only if multi-locale is ever needed" item. - (maturity-4 lever) Add an i18n translation-completeness CI gate → DONE 2026-06-30:
Tests/Architecture/MMCA.ADC.Architecture.Tests/TranslationCompletenessTests.cspairs every base.resxunderSource/with an.es.resxsibling and asserts identical key sets (36/36 today; runs in the CI.slnf arch gate). The residual code-behind English (Profile.razor.cs:38,43,101,105+EventCreate.razor.cs:60) was externalized to resources this wave. Lifted scorecard §27 maturity 3→4.- Remaining impl-7 polish → DONE 2026-07-03 (i18n completion sweep, scorecard §27 impl 7→8): MudBlazor built-in text localizes via the framework's
ResxMudLocalizer(inherited on the sweep); all residual snackbars, page titles, breadcrumbs, nav items, and both ADCHome hosts externalized (~260 new en+es key pairs across 68 resx pairs); the newLocalizedTextConventionTestsgate prevents regression; the text-expansion evidence ships upstream (Common's gallery pseudo-loc no-overflow gate covers the shared chrome). - (impl 8→9 lever) DONE 2026-07-11 (remediation wave 6):
Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/PseudoLocalizationTests.csextends the pseudo-loc evidence to ADC's own public pages (/,/conference/events,/conference/sessions): activatesqps-Plocvia the app's own/culture/setendpoint (cookie-based, because the InteractiveServer circuit's culture rides the SignalR handshake cookies, not the page query string), asserts the[!!sentinel renders without an en-US leak on a per-page resx-owned probe, and applies Common's exact no-horizontal-overflow assertion; a default-culture companion test guards the probes against drift. No host/AppHost change was needed (the culture endpoint + Development-only pseudo locale were already wired). Rides the deploy-gating chromium e2e-gate; first genuine run in CI. §27 Implementation 8→9 candidacy recorded for the next re-score. Adjudicated 2026-07-17 (twenty-first cycle): REJECTED as a partial extension (PseudoLocalizationTests.cs:51covers 3 public pages of 30+ routable pages), so scorecard §27 holds M4/I8, a verified non-move. Broadening the pseudo-loc tier across the authenticated surfaces is the open 8→9 lever. - 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.
- Remaining impl-7 polish → DONE 2026-07-03 (i18n completion sweep, scorecard §27 impl 7→8): MudBlazor built-in text localizes via the framework's
🟡 Priority 3: score 3, weight 3 (one rung from a 4)
[x] #14 · Testability & Test Strategy: 3 → 4 · RESOLVED (see IntegrationTestReworkPlan.md)
(High) The 258-test Testcontainers integration tier references the deletedRESOLVED: reworked as per-serviceMMCA.ADC.WebAPIhost, won't build, and is excluded.WebApplicationFactory<Program>tiers (Identity/Conference/Engagement, ~345 tests) over a SQL-service CI container, plus the revivedMMCA.Common.APImiddleware unit tests. In-process JWT override (forAddForwardedJwtBearer), gRPC fakes, broker InProcess short-circuit, Respawn reset. Runs viaMMCA.ADC.Integration.slnfand gates every PR (required check on an up-to-date branch; wording corrected 2026-08-23: theintegration-testsjob is PR-only,if: github.event_name == 'pull_request'atdeploy.yml:389, and protects production through branch protection, notdeploy.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.*;-*.Testscode, ratcheted to 55.5 after the 2026-07 coverage program, actual ~57%) that hard-fails the deploy-gatingbuild-and-testPR 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 SQLintegration-testsjob):Conference.IntegrationTests/CrossService/CrossServiceUserRegisteredTests.cs(BR-207 name-match auto-link / ambiguous-skip / no-match-skip) +Identity.IntegrationTests/CrossService/CrossServiceSpeakerLinkTests.cs(SpeakerLinkedToUser/SpeakerUnlinkedFromUserset/clearUser.LinkedSpeakerId). Pairs withOutboxFidelityTests(which covered the producer side only). Added via a small additiveServicesaccessor on both fixtures; compile 0/0. - [~] Phase 4 broker-transport tier (TD-02), landed 2026-07-06 as a non-gating nightly: the genuine MassTransit broker round-trip (Testcontainers RabbitMQ + dual-host transport/outbox fidelity, not just handler logic) now runs as
MMCA.ADC.CrossService.IntegrationTests(9 tests) oncross-service-tests.yml. Optional remaining coverage: speaker analytics and the Conference→Engagement bookmark-count gRPC reads. Making the tier a deploy gate is the shared §6 impl 9→10 lever (see TD-02 under #6).
[x] #11 · Security: 3 → 4 · RESOLVED
(Medium) Rate limiter is inert: named policies but noRESOLVED: MMCA.Common 1.54.0'sGlobalLimiter/[EnableRateLimiting].AddCommonRateLimitingnow attaches aGlobalLimiter(429 over 300 req/min per authenticated user; partition name→user_id→IP). Anonymous traffic is deliberately unlimited (public endpoints output-cached, login has its own protection, and Blazor-Server anonymous traffic shares the UI host IP); health//alive/JWKS/application/grpcbypassed. Swept to all 7 services (ADC + Store) on the 1.54.0 bump; CLAUDE.md "100 req/min" claims corrected.(Medium) No automated server-side authorization gate.RESOLVED: the #14 per-service tier includes the access-denied authz matrices (anonymous→401, attendee→403 across all services, ~55 tests), gating every 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 viakeyVaultUrl+ the same UAMI (Key Vault Secrets User). No plaintext Container App secrets remain.
[x] #19 · State Management & Data Flow · 3 → 4 (weight 3) · RESOLVED 2026-07-15 (twentieth-cycle re-score: scorecard §19 maturity 3→4 CONFIRMED on the StateManagementConventionTests CI.slnf gate; implementation held at 9 after a first-pass 9→8 proposal was adversarially rejected as unsupported). The 2026-07-02 reopening (no §19 fitness gate) is answered by the wave-2 gate below
(Medium) TheRESOLVED: all six Conference create forms now pass the framework live-accessorUnsavedChangesGuardparam-lag... spurious "unsaved changes" prompt after a successful createIsDirtyAccessor="() => _isDirty". The MMCA.CommonUnsavedChangesGuardlive-accessor (shipped v1.51.0) reads dirty state at navigation time, eliminating the one-render parameter lag: noStateHasChanged()-before-NavigateTodance needed.
Fix
- Adopt the framework live-accessor guard (MMCA.Common #19, shipped v1.51.0) on all six create forms; supersedes the
StateHasChanged()-before-NavigateToworkaround. - (maturity 3→4 lever) DONE 2026-07-11 (remediation wave 2): the §19 state-management fitness gate now runs in the CI.slnf arch gate:
StateManagementConventionTests(sealed subclass of the shared v1.115.0StateManagementConventionTestsBase) reflects over the three module UI assemblies (registered asLayer.UiinAdcArchitectureMap) failing the build on any mutable static field or settable static property, plus a source scan forbidding singleton*StateService/*StateContainerregistrations. Verified non-vacuous (a seeded mutable static in Conference.UI failed the gate with the exact offender name, green after removal). Landed in the same wave as the #18 gate, as planned. Candidacy CONFIRMED on the 2026-07-15 twentieth-cycle re-score: scorecard §19 maturity 3→4 (impl held 9).
[ ] #21 · Accessibility · 3 → 4 (weight 3, priority (4-3)×3=3) · scorecard §21 maturity 3 / impl 8 (2026-07-02 fifteenth cycle, re-confirmed 2026-07-03): the axe layer is an enforced deploy gate (chromium e2e-gate in deploy.needs) and the broadened 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.csruns 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 inCLAUDE.mdandACCESSIBILITY-SCREENREADER-PASS.md. (Deploy-gated since 2026-07-02: the scans ride the chromiume2e-gatejob indeploy.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 toSeverity.Infooutlined). ADC carries the same latent pattern in at leastSpeakerDashboard.razor:37andSessionFeedback.razor:29(plus amberVariant.OutlinedColor.Warningbuttons onEventDetail.razor:141and the bookmarked-state toggle onPublicSessionDetail.razor:136); the 17-page axe gate is green only because those states are not exercised by the scans. FIXED 2026-07-16 (all six sites, two more than recorded): the four outlined Warning alerts switched toSeverity.Infooutlined (Store parity; the sweep also caughtPresenterView.razor:21andSessionLive.razor:21), and the two outlined amber buttons moved to the AA-passing Secondary teal (EventDetailUnpublish, and the bookmarked state ofPublicSessionDetail's toggle, whose filled-star icon keeps the state signal). Repo-wide grep for outlined Warning surfaces is now zero. CI.slnf 2073 green. - (shared with #28) Promote the full axe + E2E suite to a merge gate → DONE (2026-07-02): promoted as the chromium
e2e-gateindeploy.needsafter validation run 28604877733 (the first fully green three-browser matrix); firefox/webkit stay advisory on the nightly (#22). (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.0UIArchitectureConventionTestsBase) caps every*.razor.csunder Source/ at 400 lines and inline@codeblocks at 120 lines. Verified non-vacuous via a seeded 402-line file. Subsumed TD-13 (below) and additionally forced conforming splits ofSessionLive.razor.cs648→357 (three extracted panels) andPublicSessionList.razor.cs499→371 (filter bar + view components), which had grown past the cap since TD-13 was recorded. Repo-wide max code-behind is now 387 lines. Candidacy CONFIRMED on the 2026-07-15 twentieth-cycle re-score: scorecard §18 maturity 3→4. - TD-13 DONE 2026-07-11 (remediation wave 2, subsumed by the §18 gate above): both named code-behinds split via presentational sub-component extraction, markup moved verbatim (rendered DOM unchanged for the E2E selectors):
SessionSelectionDashboard.razor.cs507→367 (extractedSessionSelectionSpeakerOverlap,SessionSelectionAiScores, and the pure-rulesSessionSelectionDisplayhelper) andSpeakerDetail.razor.cs429→368 (extractedSpeakerCategoryItemsPanel). Conference UI bUnit suite green (105/105) after each split. - TD-16 (recorded 2026-07-21, the §18 impl 8→9 lever, effort S): nine code-behinds sit within 38 lines of the convention ceiling, the
MaxCodeBehindLines => 400cap (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 HEAD19021d93, and the headroom is narrowing again. The high-water mark isSource/Modules/Conference/MMCA.ADC.Conference.UI/Pages/SessionSelection/SessionSelectionDashboard.razor.csat 398, up from 395, so headroom against the 400 cap fell from 5 lines to 2;SessionDetail.razor.csalso 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 HEAD995a7886; the "flush at the cap, zero headroom" framing stays retired). Re-measured 2026-08-23 (twenty-seventh cycle) at HEAD96f0919a: 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.cs367→398 (a second file at 398),ADCHome.razor.cs341→380, andEventDetail.razor.cs365→377. Re-measured 2026-08-31 (twenty-eighth cycle) at HEADb04b3a3e: the flush-at-cap state is BACK, on a file this ledger had never named. The high-water is nowSource/Modules/Conference/MMCA.ADC.Conference.UI/Pages/Session/SessionDetail.razor.csat exactly 400 of the 400 cap, zero headroom (the cap confirmed unoverridden:UIArchitectureConventionTestsBase.cs:22MaxCodeBehindLines => 400, no override in the sealed subclassUIArchitectureConventionTests.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 HEAD65bddd4b, 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 nowPages/Public/PublicSessionDetail.razor.csat 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-testnow runsdotnet ef migrations has-pending-model-changesfor all four modules (Identity/Conference/Engagement/Notification) on the Release build (--no-build, no DB needed). Fails the build (and so the deploy) if an entity changed without a matching migration. Verified locally: all four currently report "No changes" (drift-free). - Soft-delete fidelity test:
SoftDeleteFidelityTests(Conference integration tier) deletes an Event via the API, asserts it's hidden by the EF global query filter (404), and reads[Conference].[Event]directly to prove the row survives withIsDeleted = 1(soft- not hard-delete). The fixture now exposes itsConnectionStringfor raw-table assertions. - Outbox-dispatch fidelity:
OutboxFidelityTests(Identity tier) registers a user and asserts aUserRegisteredrow landed in[dbo].[OutboxMessages](confirmedInProcessEventBus.PublishAsyncpersists the row transactionally, then marks it processed, the row is retained). The Identity fixture now exposesConnectionString. (TD-04: done 2026-06-13, effort S.) - TD-03 RESOLVED (2026-07-06): optimistic-concurrency API round-trip now implemented and deploy-gated. The Conference
EventDTOcarries theRowVersiontoken viaIConcurrencyAware(Conference.Shared/Events/EventDTO.cs:16),UpdateEventHandler.cs:34stamps the client's last-seen token withSetOriginalRowVersion(a stale token then raisesDbUpdateConcurrencyException, whichDbUpdateExceptionHandlermaps to 409), andOrganizerConcurrencyTests.cs:27(Update_WithStaleRowVersion_ReturnsConflict) asserts the 409 inside the PR-gatingMMCA.ADC.Integration.slnf(wording corrected 2026-08-23: theintegration-testsjob is a required PR check, PR-only atdeploy.yml:389, not indeploy.needs). Round-trip is Conference-only (Identity/Engagement expose no token-carrying update endpoint), so scorecard §8 holds impl 9 (not 10). The Common extension point (SetOriginalRowVersionon the repository) shipped and ADC adopted it on the five Conference update handlers.
[x] #1 · SOLID Principles (3 → 4 · ctor-dependency-count fitness threshold landed (scorecard §1 stays M4/I9) protect)
(Low)GUARDED (v1.86.0 sweep, 2026-06-27): kept as the cohesive auth facade, but a ctor-dependency-count fitness function now holds the line:AuthenticationServicehas 7 constructor dependencies (down from 9: validators bundled intoAuthenticationValidators) and injects a command handler directly.Source/Modules/Identity/.../Users/AuthenticationService.cs:21-28.AuthenticationServicesits at the 7 high-water mark and an 8th dependency would fail the build.
Fix
- Acceptable as a cohesive auth facade; the ctor-dependency-count fitness threshold is now landed:
Tests/Architecture/MMCA.ADC.Architecture.Tests/ConstructorDependencyCountTests.cscaps constructor dependencies at ≤7, withAuthenticationServiceat the 7 high-water mark.
🟢 Priority 2: score 3, weight 2 (polish / hardening)
[x] #9 · API & Contract Design · Resolved 2026-06-12
(Medium) No OpenAPI served by any running service, yet CLAUDE.md still advertises 4 doc UIs (/swagger,/nswag-swagger,/api-docs,/scalar/v1).- Serve OpenAPI per service → all four service hosts now register
AddOpenApi()+ map/openapi/v1.json(built-inMicrosoft.AspNetCore.OpenApi, package wired via the.Serviceconvention inDirectory.Build.props). Mapped outside Production only: these are internal services reached through the Gateway, which does not route the endpoint. The ApiExplorer group ('v'VVV→v1) matches the default document name, so the controller surface populates. - Fixed the stale CLAUDE.md OpenAPI bullet (the four advertised UIs were a carry-over from the deleted WebAPI host; corrected to the
/openapi/v1.jsondocument). - Contract test (
OpenApiContractTestsinMMCA.ADC.Conference.IntegrationTests) boots the real host and asserts the document is served, is well-formed OpenAPI 3.x describing ≥ 10 routes, and still exposes the core public resources (/Events,/Sessions,/Speakers): so an accidental route removal fails CI. Runs in the integration-tests tier, which gates every PR as a required check (wording corrected 2026-08-23). - Versioning proven beyond v1.0 (2026-06-19).
ServiceInfoController(Conference) serves/ServiceInfoat v1.0 (deprecated) and v2.0, selected by theapi-versionheader: exercisingMapToApiVersionrouting + deprecation reporting (ReportApiVersions).ApiVersioningTests(integration tier) asserts each version returns its own shape and that theapi-supported-versions/api-deprecated-versionsheaders are emitted, so the versioning machinery is exercised, not merely configured for a single version. - 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
OpenApiContractTestsabove 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✅ fixed 2026-06-08 (CLAUDE.md broker note corrected; provider switch + Standard-tier/Manage gotchas documented).MessageBus__Provider=AzureServiceBusin prodRemaining: no ADR for the monolith→services extraction; fitness tests lag the new topology.- Correct the broker note in CLAUDE.md (it's wired in prod).
- Write the extraction ADR →
ADRs/008-service-extraction-topology.md(monolith → 4 services + Gateway; ties together the facet ADRs 003/004/006/007). README index updated. - Update fitness tests for the service topology → new
MicroserviceExtractionTests(12 tests) enforce transport-at-the-edge: no gRPC / MassTransit / Protobuf dependency in any Domain, Application, or Shared assembly, making the guard ADR-007 claimed (but that never existed) real. Full architecture suite green (110 tests, run locally: no SQL needed).
[x] #17 · DevOps & Deployment · 2 → 4 · RESOLVED 2026-06-29 (scorecard §17 maturity 4 / impl 9; managed-identity SQL auth active in prod); SQL private endpoints deferred-by-design
- (Medium) Runtime uses shared/admin keys (ACR admin password, SQL keys), not managed identity;
no rollback or post-deploy smoke gatepost-deploy smoke gate + auto-rollback added (deploy.ymlPhase 5: Gateway/health+ JWKS + UI probes →az containerapp revision copyrollback on failure; documented ininfra/DISASTER-RECOVERY.md). - Switch runtime auth to managed identity: DONE: ACR pull via the shared UAMI (admin password gone) and all runtime secrets moved to RBAC Key Vault (read via managed identity). Add a rollback path + post-deploy smoke gate → DONE. (ACR admin user disabled: no admin credential exists.)
- Switch app→DB SQL auth to managed identity (pairs with #11): DONE (2026-06-28):
useManagedIdentitySql=trueactivated in prod via the stagedinfra/SQL-MANAGED-IDENTITY.mdsequence (deploy.yml repo-var passthrough → Entra admin → per-DBCREATE USER ... FROM EXTERNAL PROVIDER+db_owner→ flag flip). All four services run passwordless onAuthentication=Active Directory Managed Identitywith asdb_ownerin every per-service DB (verified Healthy on the new revisions). The shared SQL password is gone from all connection strings; the SQL admin login is a dormant fallback. Lifts §17 impl 8→9. - (Residual, deferred-by-design) Move the SQL data plane onto private endpoints (disable public network access, drop the 0.0.0.0 firewall). The VNet + private-endpoint epic (recreates the Container Apps environment), documented-accepted in
infra/SQL-MANAGED-IDENTITY.md. This is the only remaining §11 impl 9→10 lever now that the credential flag is closed.
[x] #24 · Forms, Validation & UX Safety · 3 → 4 (weight 2) · RESOLVED 2026-06-30 for MATURITY (scorecard §24 maturity 4: FormsConventionTests in the CI.slnf arch gate enforces the unsaved-changes guard + dirty tracking + validated MudForm across the six create forms). Implementation recalibrated 9→7 on the 2026-07-03 re-score (error presentation was overstated), then recovered 7→8 on the 2026-07-15 twentieth-cycle re-score (TD-14 shipped, see below). The category header stays closed: maturity holds 4 on the gate
(Medium) Silent data loss on all six inline-edit paths (Detail pages have no unsaved-changes guard)RESOLVED:UnsavedChangesGuard(withIsDirtyAccessor) +MarkDirty/_isDirtydirty-tracking added to all six Detail edit forms (Event/Speaker/Room/Session/Question/ConferenceCategory);_isDirtyresets on edit-enter, cancel, and successful save. Build clean, 1244 ADC CI tests green.(Medium) Profile change-password form lacked client-side match/Required validation and used a generic snackbar rather than a per-form error summaryRESOLVED (v1.86.0 sweep, 2026-06-27):Identity.UI/Pages/Profile/Profile.razor:29,33,37addsRequired+RequiredErrorto all three fields,:38wires client-side match (ValidateConfirmPassword) alongsideValidateNewPassword, and:41-52renders a per-formMudAlerterror summary;Profile.razor.cs:40-43(match),:84-88(ValidateAsync gate before submit),:56/:90(Disabled while saving). Closes the last §24 scorecard deduction (impl 8→9).- Apply
UnsavedChangesGuard+ dirty tracking to the Detail/inline-edit pages (pairs with #19). - Add client-side match/Required validation + a per-form error summary to change-password: done on the v1.86.0 sweep (evidence above).
- (maturity-4 lever) Add an automated forms / unsaved-changes / validation convention fitness test → DONE 2026-06-30:
Tests/Architecture/MMCA.ADC.Architecture.Tests/FormsConventionTests.csscans the six Conference*Create.razorforms and fails the build if any drops itsUnsavedChangesGuard(with a liveIsDirtyAccessor),_isDirtytracking, validated<MudForm, orRequired/RequiredErrormarkers (runs in the CI.slnf arch gate, verified green). Lifted scorecard §24 maturity 3→4. - TD-14 CLOSED 2026-07-11 (remediation wave 6), the §24 impl 7→8/9 lever: both remaining pieces landed. (a) All six Conference create forms now render the same per-form
MudAlerterror summary the Profile form pioneered (localizedValidation.CorrectFollowingheading + the_form.Errorslist, en+es key pairs added to all six form resx pairs; the snackbar kept as the secondary channel). (b)FormsConventionTestsnow covers the Profile form via a dedicated fact (error summary +ValidateNewPassword/ValidateConfirmPasswordwiring + the three Required password fields) AND hardens the create-form gate by appending the error-summary markers toRequiredMarkers, so the new presentation cannot silently regress. CI.slnf 2066 tests green. Candidacy CONFIRMED on the 2026-07-15 twentieth-cycle re-score: scorecard §24 impl 7→8 (held at 8, not 9: the summary conventions are string-marker enforced; a render-level bUnit assertion of the summary's error items is the 8→9 lever). (Historical: the raw{ex.Message}snackbars were eliminated on the 2026-07-03 i18n sweep.) 8→9 lever SHIPPED 2026-07-16:EventCreateTests.SubmittingBlankForm_RendersThePerFormErrorSummaryWithItemsrenders the form, fails validation, and asserts the summaryMudAlertactually renders with its localized heading and per-error list items (render-level proof beside the string-marker gate). Runs in the CI.slnf bUnit tier. §24 impl 8→9 candidacy recorded for the next re-score.
[x] #13 · Observability & Operability · 3 → 4 (weight 2) · RESOLVED 2026-07-17 (twenty-first-cycle re-score: scorecard §13 maturity 3→4 CONFIRMED on the ObservabilityConventionTests alert-runbook pairing gate in the CI.slnf arch gate, MMCA.ADC.CI.slnf:56 + deploy.yml:57,417; implementation holds 9). The 2026-07-15 REOPENING is closed: the exact lever it named (a CI gate over the sloAlertSpecs/OPERATIONS.md pairing) shipped 2026-07-16
(Medium) No alerting / SLOs / dashboards / runbooks (App Insights is wired but passive).- Add alerts + action groups, dashboards, and basic runbooks (overlaps #29). → alerts + action group done (3 App-Insights SLO metric alerts in
main.bicep); recovery runbook done (infra/DISASTER-RECOVERY.md); dashboard/workbook done (sloWorkbookAzure Monitor workbook inmain.bicep←workbooks/adc-slo-workbook.json, mirroring the SLO alerts per service). - Day-2 operational runbooks DONE (2026-07-11):
infra/OPERATIONS.mdmaps each provisioned alert (failed-requests,server-response-time,dependency-failures) to concrete triage steps (workbook pane, App Insights drill path, per-service container logs, the auth/gRPC/outbox failure classes) plus the fast-reference recovery moves (revision rollback, PITR restore, the three freshness gates, surge revert) and a pair-with-sloAlertSpecsgovernance note. Adjudicated 2026-07-15: the runbook/workbook substance lifted scorecard §13 impl 8→9, but the maturity 3→4 candidacy was rejected (review-enforced, not CI-gated); the category stays open at M3/I9. - Maturity gate SHIPPED (2026-07-16, the reopened lever):
Tests/Architecture/MMCA.ADC.Architecture.Tests/ObservabilityConventionTests.csmachine-enforces the alert-to-runbook pairing in the CI.slnf arch gate (runs on every PR and gates deploy): everysloAlertSpecsentry ininfra/main.bicepmust keep a### ...-alert-<key>section ininfra/OPERATIONS.mdwhose heading carries the alert's current(sev N), orphan runbook sections fail, and a minimum-spec floor (3) keeps the parse non-vacuous. Both files are embedded resources of the test assembly, so the gate sees exactly what ships. Verified red on a seeded severity drift (sev 2 to 4 flagged with the exact heading) and green on the real files. TheOPERATIONS.md"change a threshold and this file together" governance note is now enforced, not advisory. Maturity 3→4 candidacy recorded for the next re-score. Mirror planned for Store #13 (same gate shape). Adjudicated 2026-07-17 (twenty-first cycle): ACCEPTED; scorecard §13 M4/I9, category closed (protect).
[~] #22 · Responsive & Cross-Browser · 3 → 4 (weight 2, priority (4-3)×2=2) · REOPENED 2026-07-21 (twenty-second-cycle re-score: scorecard §22 maturity 4→3, implementation holds 8). The 2026-07-16 lever that closed this item was undone on 2026-07-18 by the Actions-minute reduction: the deploy e2e-gate now invokes browsers: '["chromium"]' (deploy.yml: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'se2e-gatewas cut tobrowsers: '["chromium"]'on 2026-07-18 for Actions-minute savings (job atdeploy.yml:628withbrowsers: '["chromium"]'at:638, still indeploy.needsat:992; anchors refreshed 2026-08-31), so firefox/webkit now run only on the alternating single-engine nightlyschedule(cronse2e.yml:49,:50), wheree2e.yml:144keeps themcontinue-on-error. Options: (a) re-add the two legs to the deploy gate (3x runner minutes, the 2026-07-16 shape), (b) add across-browser-freshnessjob todeploy.needsmirroring the existing dr / load / cross-service freshness gates (deploy.yml: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 (RESOLVED: the 18 master-data management routes now carry[Authorize]only, no role attribute).@attribute [Microsoft.AspNetCore.Authorization.Authorize(Roles = "Organizer")]: Event/Session/Room/Question/ConferenceCategory (list+create+detail), Speaker (list+create), and IdentityUserList(/users). These already had the server-side gate ([Authorize(Policy = AuthorizationPolicies.RequireOrganizer)]on the controllers, reads[AllowAnonymous]); the route attributes were the missing UI/IA layer, so this is defense-in-depth + no more attendee-visible dead-end pages. Build clean (0/0).- Deliberately left bare
[Authorize]:SpeakerDetail(/speakers/{id}): PUT/speakers/{id}is[Authorize](ownership-checked) so a speaker self-edits their own profile there;SpeakerDashboard, Engagement feedback, and IdentityProfile/UserClaimsare self/attendee-facing.
- Deliberately left bare
- Add role-based authorization (
[Authorize(Roles)]) to admin routes. - Residual: DONE.
ManagementRouteAuthorizationTests(reflection fitness test inConference.UI.Tests) asserts every admin-namespace page keeps[Authorize(Roles="Organizer")]so a route can't silently drop to bare[Authorize]. IdentityUserList(/users) is covered by the parallelIdentityRouteAuthorizationTestsin the newIdentity.UI.Testsproject.
[x] #6 · CQRS & Event-Driven · 2 → 4 · RESOLVED 2026-06-29, scorecard §6 maturity 4 / impl 9 as of the 2026-07-02 re-score: impl corrected 10→9 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 everyIIntegrationEventin the module Shared assemblies and snapshots its declared shape (property name + type) against a frozen baseline. A renamed/removed/retyped property (or a new event added without snapshotting) fails the build, forcing a conscious version/rollout decision. The async counterpart to #9's REST contract test; runs in CI build-and-test (no broker/SQL needed). Verified locally (111 architecture tests green). - Idempotent inbox enabled on the consumers (2026-06-19).
MessageBus:EnableInbox=trueinIdentity.Service+Conference.Serviceappsettings (the two services that consume integration events; each already ships theInboxMessagestable via itsAddInboxMessagesmigration). Dedup is now verified in MMCA.Common byEfInboxStoreTests(real SQLite + the production unique index → a redelivered message id records exactly once). Converts consumer idempotency from convention to infrastructure. - *§6 Implementation 9→10 lever, TD-02 CLOSED 2026-07-11 (remediation wave 6):* both remaining pieces landed. (1) The broker round-trip now gates the deploy via recency: a
cross-service-freshnessjob indeploy.yml'sneedsfails a deploy when the latest successful nightlycross-service-tests.ymlrun is older than 3 days (the dr/load-freshness pattern; the Testcontainers workflow itself still never runs inside the deploy chain, which the Docker constraint forbids and its header comment now documents). (2)MessageBus:EnableInbox=trueon all four consumer services: Engagement and Notification appsettings joined Conference + Identity (theirInboxMessagestables shipped with the 2026-06-09AddInboxMessagesmigrations, applied in prod by the sole-migrator startup path). §6 Implementation 9→10 candidacy recorded for the next re-score. (Historical context: the tier landed 2026-07-06 as 9 Testcontainers RabbitMQ+SQL dual-host tests.)
[~] #12 · Performance & Scalability · 3 → 4 (weight 2, priority (4-3)×2=2) · OPEN at scorecard §12 M3/I8 (twentieth-cycle adjudication, re-confirmed 2026-07-17; a stale nineteenth-cycle "RESOLVED 2026-07-12 M4/I8" header accidentally committed via PR #15 is corrected here). The k6 proof's recency gates the deploy (load-freshness, deploy.yml: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 testing→ DONE: the k6conference-read-load.jsload test runs in CI sized to the measured ~67 peak. The SignalR multi-replica/backplane risk is resolved into a documented single-replica acceptance (Notification pinnedmaxReplicas: 1,main.bicep:1007-1012).(impl-8 lever) Add client-side Core Web Vitals measurement to the E2E suite → DONE 2026-06-30: a
WebVitalsTestsPlaywright tier (Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/WebVitalsTests.cs+Infrastructure/WebVitalsCollector.cs) injectsPerformanceObservers to capture LCP/CLS/FCP/TTFB on/,/conference/events,/login(plus a single-interaction INP sample on the data-grid page), asserts lenient budgets, and emits a datedweb-vitals-*.jsonartifact (wired intoe2e.ymlviaWEB_VITALS_OUTPUT_DIR). Both the backend k6 and the client-side vitals are now measured, closing the residual gap and lifting scorecard §12 Implementation 7→8. Test/CI-only (noMMCA.Commonrelease); builds clean. (Maturity held at 3: the vitals run nightly/dispatch like k6, not as a merge gate.)Deferred (optional), provisioning half DONE: prod Redis is provisioned (
infra/main.bicep:740Microsoft.Cache/redisEnterprise@2024-09-01-preview, database at:753,redis-connection-stringsecret injected at:771,849-850), so the shared cache / SignalR backplane substrate exists. The fan-out itself stays unexercised: Notification is still pinnedmaxReplicas: 1(infra/main.bicep: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_failed0.00%, p95 139.89 ms against the 800 ms budget; Store run 33589272010 green the same night), soload-freshnessholds 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 200840/21325,http_req_failed96.06% against the thresholdrate<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 commit79b3dcf8) rejects a single-runner k6 driving roughly 101 req/s from one IP. The last green run is 2026-08-01 (run 30688514601), andload-freshnessis a 35-day recency check on the last successful run (deploy.yml:756-806,FRESHNESS_DAYS: "35"at:764) sitting indeploy.needs(:1054), so every ADC deploy fails from 2026-09-05 unless the run goes green or theskip_freshness_gatesbreak-glass (with a mandatoryskip_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 becauseload-test.ymlpromises never to mutate production): MMCA.Common gains a secret-gated synthetic-traffic bypass on the edge limiter (a request whoseX-Synthetic-Traffic-Keyheader 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, injectsGatewayRateLimiting__SyntheticTrafficSecretinto the gateway from Key Vault the same way it injects the SMTP password, sends the header fromload-test.ymlvia 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-freshnessjob indeploy.yml'sneedsfails the deploy when the latest successful monthlyload-test.ymlrun is older than 35 days (the dr-freshness pattern; latest run 2026-07-01, green), and (b) the WebVitals budgets were tightened from catastrophic-only (LCP 8000) to the Core Web Vitals "good" band (LCP 2500 / FCP 1800 / TTFB 800 / CLS 0.1 / INP 500), calibrated against measured CI maxima (LCP 624ms, 4-30x headroom), asserted inside the deploy-gating chromiume2e-gate(e2e.yml runs the whole E2E project; 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 pinnedmaxReplicas: 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.ymlagainst 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 (theIUiReadCachewithdrawal 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:31fails the build when a handler/validator is stranded from its same-assembly contract. Scorecard §5 reaches maturity 4, impl held at 8 by the conscious layered-by-project hybrid.
[x] #16 · Maintainability & Evolvability · 3 → 4 (weight 2) · RESOLVED 2026-06-30 (scorecard §16 maturity 4 / impl 8): FrameworkVersionConsistencyTests (CI.slnf arch gate) now fails the build if any MMCA.Common.* package diverges from the single lockstep version, so ADR-016 consistency is enforced not merely followed
lingering non-building test projectsRESOLVED:Tests/WebAPIrevived asMMCA.Common.APImiddleware unit tests; the orphaned combinedMMCA.ADC.IntegrationTestswas superseded by the per-service integration projects (#14) and, once its single-service tests were re-homed and its headline cross-service flows restored (#14 Phase 4), the project folder was physically deleted: so no non-building legacy test csproj remains in the tree (the orphan-test cleanup shipped, but the 2026-06-29 re-score holds §16 at maturity 3: process is Consistent, not yet fully Optimized).- Tech-debt tracking: every deferred sub-item carries a
TD-NNID with its blocker + resolution path + effort, and the recorded-not-scheduled choices have a Deliberate / accepted section. (Originally a separateTECHDEBT.md(TD-01…TD-10); folded into this backlog 2026-06-26 as the single per-repo ledger, matching MMCA.Common and MMCA.Store.) - Doc drift (#34): fixed (broker note corrected; extraction ADR + fitness tests landed under #34).
- (maturity-4 lever) Reach Optimized §16 process maturity → DONE 2026-06-30: added
Tests/Architecture/MMCA.ADC.Architecture.Tests/FrameworkVersionConsistencyTests.cs, a fitness check that readsDirectory.Packages.propsand fails the build if the thirteenMMCA.Common.*packages are not all pinned to one version (catching a partial sweep), so the lockstep-version consistency is enforced not merely followed. The remaining residual is cosmetic (the frozen combinedMMCA.ADC.Migrations.SqlServerarchive csproj; the workspaceArchitecturalAnalysis.mdoutside any repo), acceptable.
[x] #20 · Design System & UI Consistency · 2 → 4 · RESOLVED 2026-06-29 (scorecard §20 maturity 4 / impl 9); residual Secondary-token / !important drift is Common-side (see Deliberate / accepted)
Landing page hardcodes brand hex and is duplicated across two hosts; no automated consistency check.RESOLVED (v1.86.0 sweep, 2026-06-27): the ADC landing page is now brand-token-clean:ADCHome.razor.css:215,252,277in both UI hosts usevar(--mmca-primary), guarded byTests/Architecture/MMCA.ADC.Architecture.Tests/BrandColorTokenTests.cs:26-37(a consistency fitness function). Lifted scorecard §20 impl 8→9.- Centralize the brand token; dedupe the landing page; add a consistency check. → done (evidence above).
- Residual (Common-side, OPEN):
BrandColorTokenTestsguards Primary only (Secondary has no drift test), and a few!importantoverrides + Store-specific cart CSS live in Common's sharedapp.css. These are MMCA.Common changes, not ADC-local.
[x] #23 · Front-End Performance · RESOLVED 2026-07-15 for MATURITY (twentieth-cycle re-score: scorecard §23 maturity 3→4 CONFIRMED on the enforced CWV budgets inside the deploy-gating chromium e2e-gate; implementation holds 8, the code-split/image polish below stays open)
No Core Web Vitals/RUM; WASM not code-split; images unoptimized.- Add CWV tracking → DONE + GATED (2026-07-11, remediation wave 3): CWV was measured per E2E run since 2026-06-30 (
WebVitalsTests); the budgets are now the enforced Core Web Vitals "good" band asserted inside the deploy-gating chromiume2e-gate(see the #12 wave-3 note above; 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/budgetsinmain.bicep: a monthly RG budget (monthlyBudgetAmount, default $200) notifying the existing action group +alertEmailAddressat 80% actual and 100% forecasted spend. The automatic guard against an un-reverted surge silently billing for weeks. - Resource cost tags:
commonTags(application/environment/component/managedBy/costCenter) stamped on every billable resource acrossmain.bicep(App Insights, SQL server + all 5 DBs, Service Bus, Container App env, all 6 Container Apps) andfoundation.bicep(ACR, Log Analytics) for Cost-Analysis attribution. - Scheduled surge-drift guard:
.github/workflows/cost-guard.yml(weekly cron + manual) is a read-only check that everyadc-*Container App is ≤maxReplicas 2and every SQL DB is Basic; on drift it fails the run (GitHub-notifies) and prints how to reset. Read-only by design: auto-mutating prod on a schedule would clobber an intentional surge and risks revision churn; the budget covers the $ side, this covers the config side. - (maturity-4 lever) Lift FinOps process maturity beyond the scheduled read-only
cost-guard.yml→ DONE 2026-06-30: added aworkflow_calltrigger tocost-guard.ymland acost-guardjob (uses: ./.github/workflows/cost-guard.yml,secrets: inherit) todeploy.yml'sdeploy.needs, so the read-only surge-drift check now gates the deploy (a deploy is blocked while a conference-day scale-up is un-reverted) rather than only flagging weekly. Lifted scorecard §31 maturity 3→4. Live indeploy.needssince 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:
AddBrokerMessagingfalls back to the in-process bus when no broker is configured,DataSourceResolvercollapses the per-module logical sources onto one physical database (single context, FK constraints restored), andModuleLoaderboots all four modules in one host behind no gateway (MMCA.Helpdesk is the living single-host proof). Collapsing production would cut the$190-220/mo run cost to roughly a third and eliminate the gRPC partial-failure class (peer-not-ready, mixed-endpoint quirks, best-effort degradations) outright. Monthly cost drivers today: ACA ~$110-130 (6 apps, min 1 replica each), SQL ~$25 (5 Basic DBs), Log Analytics ~$25, Redis ~$13 (Balanced B0), Service Bus ~$10 (Standard). 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 Enterprise76 accounts / ~67 peak) would be comfortably served by one host. Resolution path when revisited: single service host enabling all modules (Helpdesk pattern), oneBalanced_B0(infra/main.bicep:864-869), Service BusStandard(: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 (ADCdatabase via the resolver collapse (migrate the four DBs' data in), drop Service Bus/Redis/Gateway resources frommain.bicep, point the UI at the host directly, and re-run the k6 capacity proof at the collapsed tier. - Note: both Bicep templates validated locally with
az bicep build(clean). The new budget params default sensibly, so nodeploy.yml/main.parameters.jsonchange is required.
[x] #32 · Dependency & Supply-Chain Management · RESOLVED (single-axis 3 → 4; two-axis M3→4 / I7→8→9 as of the 2026-06-29 re-score); only a direct MassTransit pin remains for impl 10
- (Vulnerability scanning is active: NuGetAudit gates restore, which caught the MessagePack CVE; now also a blocking PR
supply-chainjob.) - Enable lock files, add an SBOM step, add license scanning. → DONE (TD-01 closed, 2026-06-26): 58 committed
packages.lock.json(65 as of 2026-07-23;RestorePackagesWithLockFile=trueinDirectory.Build.props:27; the Blazor WASM client + UI.Web host opt out viaRestorePackagesWithLockFile=false: sidestepping the NETSDK1124 trimming-check that wedged the earlier bootstrap), and thesupply-chainCI job's vuln-audit + SBOM are now blocking PR gates (deploy.yml:108-169,:146/:155exit 1, indeploy.needs); license/deprecated reports stay advisory. Residual (now keeps two-axis impl at 9, not 10): the--locked-modehalf is DONE (CI restore runs--locked-modein both gating jobs,deploy.yml:40/:119, so lock-file drift is tamper-enforced at restore, lifting scorecard §32 impl 8→9 on the 2026-06-29 re-score); the only remaining open sub-part is that MassTransit v8 is still pinned only transitively via MMCA.Common, not in ADC's own props.
[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_TOKENbootstrap → DONE:README.mdis now a full getting-started guide (prerequisites incl. Docker, theGITHUB_TOKENpackages:readbootstrap with the local-sourcelocal.propsalternative and the stale-Debug-DLL gotcha, run/test commands incl. MTP filter syntax, fixed local endpoints). - Close (not just record) the local-vs-prod broker parity gap → the gap is recorded, mitigated, and referenced (the README's parity section documents RabbitMQ-local vs Service-Bus-prod and points at the nightly Testcontainers broker round-trip whose recency gates deploys, TD-02), but closing it needs either a local Service Bus surface (e.g. the Service Bus emulator in the Aspire AppHost, or an opt-in cloud-broker local profile) or an automated Service-Bus-behavior test tier; documentation alone holds §33 at M3/I8. CLOSED 2026-07-16 via the automated Service-Bus-behavior test tier:
Tests/Integration/MMCA.ADC.ServiceBusEmulator.IntegrationTestsruns MassTransit v8 against the official Service Bus emulator (pinned 2.0.1, the first line with the admin plane MassTransit's topology provisioning needs) with ADC's REAL integration-event contracts, proving admin-plane topology creation + the AMQP publish-to-consume round-trip nightly incross-service-tests.yml(a new job in the same workflow, so its result rides the existingcross-service-freshnessdeploy gate). Design notes: MassTransit v8 has no vendor emulator mode (v9-only; excluded by the v8 policy pin), so the tier uses the public custom-clientsHost()overload, its own test process (the emulator's 1h TTL quota requires overriding process-global MassTransit defaults), and one warm container (10-connection + admin-throttle quotas). Deliberately a smoke, not a port of the 9 RabbitMQ round-trips: the RabbitMQ tier keeps the outbox/inbox pipeline coverage; this pins the transport. §33 M3→4 + I8→9 candidacy recorded for the next re-score. - 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-smokeatcross-service-tests.yml:153carries nocontinue-on-error(the only one left in the file isapphost-smokeat:204) under the "AUTHORITATIVE SINCE 2026-08-31 (TD-17)" header at:126-137, whose:135forbids re-adding it;cross-service-freshnessrequires bothcross-serviceandservicebus-emulator-smoketo have concluded success in the same run (deploy.yml:874), fails the deploy otherwise (:885), sits indeploy.needs(:1054) and is required at:1089; and the local parity profile exists atAppHost/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-smokeruns on the weekday nightly again (cross-service-tests.yml:145for the job,needs: should-runat:146+if: needs.should-run.outputs.run == 'true'at:147, under the workflow'sworkflow_dispatchat:26pluscron: '0 6 * * 1-5'at:31,timeout-minutes: 10at:149), restored 2026-07-29. Anchors corrected 2026-08-14 (the recorded:144-146/:26,:30/:148had drifted). The recorded blocker was wrong and is corrected here: the RESCHEDULED comment at:130-143documents the real cause as per-test bus re-provisioning against an admin plane throttled at roughly 1 op/sec (IAsyncLifetimeplus 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 iscontinue-on-error: true(:150) and rides no gate, becausecross-service-freshnesskeys off thecross-servicejob, not this one (:126-129; gate job atdeploy.yml:760, re-anchored 2026-08-31; the job/cron/timeout/continue-on-error anchors:145/:146/:149/:150re-confirmed this run). So the Service Bus signal is advisory: informative, not authoritative. Caution: the "verified by three consecutive dispatches" line at:143is 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): thecontinue-on-errorwas 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) |
| §4 | 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). 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 | 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. maxReplicas: 1maxReplicas: 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): GHSA-2m69-gcr7-jv3qDirectory.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); NoWarn codesCS1591;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 | 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) MudForm instancesMudForm 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.slnflists onlyUI.Web(:25) andUI.Web.Client(:26); no workflow installs themaui-androidworkload, soMMCA.ADC.UIis never compiled in CI and its analyzers,TreatWarningsAsErrorsand its ownNoWarn 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 againstCI.slnftoo (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: theDirectory.Build.props:8-12System.Private.Urisuppressions this item used to cite no longer exist (that file now carries zeroNuGetAuditSuppressitems and no GHSA id), so the "the suppressions exist for a graph never audited" framing is retired; and a new.github/workflows/maui-audit.ymlaudits 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.ymlreturns 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 themaui-androidworkload 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 --vulnerableover 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 isCI.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-54carries theModule("Notification", ...)entry, soMMCA.ADC.Notification.Application/.API/.Sharedare 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 noModule("Notification", ...)entry at all (Tests/Architecture/MMCA.ADC.Architecture.Tests/AdcArchitectureMap.cs:12-43; its doc comment at:4-5names only Identity, Conference and Engagement), soMMCA.ADC.Notification.Application/.API/.Sharedsit 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-gateruns 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 thedeployjob 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 theuiscoping 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 atdeploy.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 newbackend-test-gate(deploy.yml:394-396, whoseifis the exact complement ofe2e-gate's, indeploy.needsat:1054and required at:1093, running the CI.slnf unit + architecture + bUnit tier with no Playwright browsers). The browser half is unchanged and stays open:e2e-gateis still ui-scoped (:688) anddeploystill 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:15states 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 pinningMinimumPathCount=10and/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
AddOutputCachewith no store while runningmaxReplicas: 2, so everyEvictByTagAsyncreached 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 asIDistributedCache. 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 everyPublicSessionDetailview viaCategoryItemLookupService, so each view was an uncached full-table read),SessionCategoryItems,SpeakerCategoryItemsandEventSpeakersnow carry their parent's policy and evict both parents' tags on mutation, which they previously could not do at all (noIOutputCacheStorewas injected).SessionSpeakersalready evicted correctly and only needed the attribute. Correction to the deferred item, which listed seven controllers:EventQuestionAnswersandSessionQuestionAnswersare[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.
SessionScoringProcessorevicted the rootconferencetag 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. Nowconference: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.SessionQuestionAnswersandEvent.EventQuestionAnswerswere[Navigation], soincludeChildren=trueshipped 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 organizerSpeakerDetailandSpeakerDashboardServiceeach 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 virtualSpeakerIdfilter on the paged endpoint (theSpeakersControllerEventIdprecedent) resolves theSessionSpeakerjoin to aSession.Id IN (...)specification, ANDed with the BR-132 public filter, never substituted for it. -
LivePoll(SessionId, Status)indexed.GetOpenPollsHandlerfilters on both, which is the session Live page and presenter view read once per attendee per structural poll event, and onlyEventIdwas indexed. - Prerender double-fetch guarded on
PublicSessionDetailandPublicSpeakerDetail. - Load and CWV coverage.
conference-read-load.jsexercised onlyincludeChildren=falsepaths, which is why the feedback-payload regression was invisible to load evidence; it now also reads theincludeChildren=trueshapes 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/SessionFeedbackloop 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.
SessionLivefetches event-wide polls and filters client-side. Not a type-compatible swap:GetEventPollsAsyncreturnsLivePollDTOwhile the session-scoped call returnsLivePollResultsDTO, and the event list is organizer-only today with speakers deliberately falling back on 403, so it needs an authorization decision too. - TD ·
GetOpenPollsHandlerissues two queries per poll. The same N+1 shape theSessionQuestionViewBuilderfix 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 ininfra/DISASTER-RECOVERY.md. The weeklycost-guard.ymlwould flag a surge that was applied and not reverted. - No interactive OpenAPI UI (Scalar/Swagger): the h2c-only REST services aren't browser-reachable directly; a Gateway-routed UI is a small follow-up if/when wanted (#9).
- Legacy pre-cutover database retained: kept untouched (Basic tier) as the rollback/archive source; never written to after the per-service-DB cutover. Intentional.
- Integration events published post-commit carrying DB-generated IDs: intentional (the event must carry the persisted identity); not debt (#6).
- #1 SOLID (
AuthenticationService7-ctor-dependency cohesive auth facade) accepted as-is; the ctor-count fitness threshold (ConstructorDependencyCountTests, ≤7) is now landed on the v1.86.0 sweep, so #1 is closed (scorecard §1 stays M4/I9). - #5 Vertical Slice, deliberate layered-by-project hybrid: cross-cutting handled in the decorator pipeline; the hybrid is the accepted choice that caps implementation at 8, and the slice-cohesion line is held by a CI-gated fitness test (
SliceCohesionTests, inMMCA.ADC.CI.slnf) that lifted scorecard §5 to maturity 4 (#5 closed, scorecard §5 M4/I8). Scoping clause (2026-08-14): this accepted hybrid is the impl-8 cap, and it does not absorb TD-19. The absence ofMMCA.ADC.Notification.*fromAdcArchitectureMap.cs:12-43is an enforcement-coverage gap and schedulable work, not an accepted trade-off. - #20 Design System Common-side residuals: accepted as out-of-ADC-scope:
BrandColorTokenTestsguards the Primary token only (Secondary has no drift test), and a few!importantoverrides + Store-specific cart CSS live in MMCA.Common's sharedapp.css. These are MMCA.Common changes, not ADC-local; ADC's §20 is maturity 4 / impl 9. - Chromium-only deploy E2E gate (recorded 2026-07-18, CI-minute reduction; amended 2026-08-01):
deploy.yml'se2e-gateinvokes one browser leg instead of three (the job is atdeploy.yml:628withbrowsers: '["chromium"]'at:638; anchors refreshed 2026-08-31 from the drifted:531/:541, substance re-confirmed and the job is still indeploy.needsat:992); firefox/webkit cross-engine coverage moved to the nightlye2e.yml, wherecontinue-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 (across-browser-freshnessgate) 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-gateruns only when thechangesjob'suioutput is true (deploy.yml:688, job:677, rationale:682-687; anchors refreshed 2026-09-01) anddeploytreats 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 complementarybackend-test-gatejob now runs the CI.slnf unit + architecture + bUnit tier (no Playwright browsers) on exactly the deployse2e-gateskips (deploy.yml:394-396, itsifthe exact complement ofe2e-gate's, indeploy.needsat: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 askip_freshness_gatesworkflow_dispatch input with a required justification (declared atdeploy.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 aredr-freshness :549,load-freshness :606,cross-service-freshness :760alongsidecost-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 designRETIRED 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-smokecarries nocontinue-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), andcross-service-freshnessrequires both broker jobs to have concluded success (deploy.yml:874), failing the deploy otherwise (:885), indeploy.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. Theskip_freshness_gatesbreak-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:145for the job,needs: should-runat:146+if: needs.should-run.outputs.run == 'true'at:147, underworkflow_dispatch:26+cron: '0 6 * * 1-5'at:31,timeout-minutes: 10at:149; anchors corrected 2026-08-14). The RESCHEDULED comment at:130-143also 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 (IAsyncLifetimeplus 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 iscontinue-on-error: true(:150) and advisory by design, and nothing gates on it, sincecross-service-freshnesskeys off thecross-servicejob (:126-129, gate atdeploy.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
PseudoLocalizationTestsbeyond 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@pagefiles (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 sincec5e6f653(2026-07-11) and no.resxhas 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 beyondes, any RTL locale, or a reported layout regression on an authenticated page. Maturity 4 is unaffected and remains doubly CI-gated (TranslationCompletenessTests+LocalizedTextConventionTests, both inMMCA.ADC.CI.slnf:58, run atdeploy.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.ValidateRoomAssignmentAsyncis 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 tomaxReplicas: 2;sp_getapplockneeds 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 theExistsAsynccall (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; theIUiReadCachenon-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-rootArchitecturalAnalysis.mdremains the already-weighed 9-not-10 lever). Each is a verified non-move (score held), not a closure. Update (2026-07-15 twentieth-cycle full re-score): the FLAG set shifted again. §12 returns plain CONFIRMED (M3/I8, no longer flagged) and §23 exits as a lift (maturity 3→4 on the now-enforced CWV budgets, superseding its nineteenth-cycle rejection). This cycle's adversarial adjudications: §19 (a first-pass impl 9→8 downgrade was rejected as unsupported while the maturity 3→4 lift was confirmed, so §19 closes at M4/I9), §28 (M4/I8 verified non-move, but its row carried a materially false claim now corrected in place: E2E #5 is re-quarantined atSpeakerSelfServiceTests.cs:57, not "un-skipped/active", plus three drifted line anchors), §33 (M3/I8: a proposed impl 8→9 was rejected on the open broker-parity red flag,README.md:74), and §34 (M4/I9: the identical impl 9→8 downgrade re-proposed and re-rejected). Each non-move is a held score, not a closure. Update (2026-07-17 twenty-first-cycle full re-score): one FLAG this cycle: §27 (M4/I8 verified non-move: the recorded impl 8→9 candidacy, extending the pseudo-loc text-expansion evidence to ADC pages, was adversarially rejected becausePseudoLocalizationTests.cs:51covers only 3 public pages of 30+ routable pages, a partial extension; §27 stays in the protect set at its held score). §12 and §33 return plain CONFIRMED at M3/I8 (their twentieth-cycle adjudications re-derived from fresh evidence, including theload-freshnessgate and the Service Bus emulator tier, neither sufficient for a move). A FLAG is a held score, not a closure. Update (2026-07-21 twenty-second-cycle full re-score): the single FLAG is again §27 (M4/I8): the identical impl 8→9 pseudo-loc candidacy was re-proposed and re-rejected on unchanged evidence (PseudoLocalizationTests.cs:51covers exactly 3 public pages against 36 routable pages), so it stays a verified non-move in the protect set. The §33 sentence in earlier updates that quotedREADME.md:74is superseded: that admission no longer exists in the file (see the #33 header for the rewritten basis). Update (2026-07-23 twenty-third-cycle full re-score): the FLAG set shifted: §27 returns plain CONFIRMED (M4/I8, in the protect set; the impl 8→9 pseudo-loc candidacy was not re-proposed this cycle). The two verified non-moves are §12 (M3/I8: a proposed maturity 3→4 was adversarially rejected because the k6 capacity proof executes monthly/dispatch out of band withload-freshnessa recency-only check,deploy.yml:553, and Notification stays pinnedmaxReplicas: 1,infra/main.bicep:1424) and §21 (M3/I8: a proposed maturity 3→4 was rejected because the manual screen-reader pass is still unrecorded inACCESSIBILITY-SCREENREADER-PASS.md, the cheapest maturity 3→4 lever). §22 and §33 are plain CONFIRMED at M3/I8. A FLAG is a held score, not a closure. Update (2026-07-28 twenty-fourth-cycle full re-score, pin v1.131.0, HEAD2ec77796): 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 globalNoWarncodes, 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, HEAD995a7886): 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, andAdcArchitectureMap.cs:12-44omitsMMCA.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 atinfra/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 iscontinue-on-errorand 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, HEAD19021d93): 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 andAdcArchitectureMap.cs:12-43still 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 touchedload-test.yml,deploy.ymlorTests/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 refreshedinfra/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, HEAD96f0919a): 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 onOrganizerContactEmail; 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 atadc-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 sharedMarkupSnapshothelper 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.ymlbyte-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, HEADb04b3a3e): 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, andEventsController.csa 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 freshIUiReadCachewithdrawal 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 atinfra/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 21MudForminstances). §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, HEAD65bddd4b): 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 ofEvent's optional fields and the raw-stringOrganizerContactEmailremain). §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 atinfra/main.bicep:1596on the verifiedTwoReplicaHubFanOutTests.cs:49proof, 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 atadc-ACCESSIBILITY-SCREENREADER-PASS.md:62; axe still E2E-only, chromium-only, non-PR, conditional). §22 8→9 rejected (chromium-only atdeploy.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 (theEmailvalue object insideEvent, 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.mdstill 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
- ✅ Tokens out of
localStorage+ CSP (#26): cookie-only refresh + OAuth code-exchange + enforced CSP shipped. (Residuals: Gateway headers; the Option-A-vs-C+ BFF decision is pending the user.) - ✅ Rework the orphaned integration tier (#14): per-service WAF tiers, ~345 tests, deploy-gated.
- ✅ Real erasure path + stop logging PII (#30):
IAnonymizable+ anonymize-on-delete + export endpoint + log redaction. (Residual: cross-service export aggregation.) - ✅
UnsavedChangesGuardsweep (#19 + #24) and admin-route authz (#25). - ✅ bUnit + axe harness, E2E as a merge gate (#28 + #18 + #21): the bUnit tier shipped earlier; the chromium E2E/axe suite became the deploy-gating
e2e-gateon 2026-07-02 (#28 closed, TD-06/07 done). (Residuals: the #18/#19 UI fitness gates and the #21 recorded SR pass.) - Credential hardening (#11 rate-limiter [Common] + #17 Key Vault/managed identity) and observability (#13/#29 alerts, RTO/RPO, LTR backups), then doc/CLAUDE.md drift (#9, #34).
Current top levers (2026-06-30, after the enforcement-gate wave): the five "good-but-not-a-gate" maturity items that were the prior top levers (#16/#24/#27/#29/#31) are now closed by CI-enforced gates. The remaining OPEN levers are the front-end-E2E cluster, all gated by one blocker: #21 Accessibility (priority 6, the single highest-leverage open item: the SR pass is recordable now to reach maturity 2→3, but the axe merge gate for maturity→4 is blocked), #28 (promote E2E/axe to a merge gate), and #22 (cross-browser pass). All three are blocked by the same diagnosed Blazor-Server-under-load E2E limit (see the #28 root-cause note), so the gate path is a slow-pace or dedicated-CPU runner, not another test fix. The cheapest open win is the recorded manual screen-reader pass (#21 maturity 2→3, ACCESSIBILITY-SCREENREADER-PASS.md), which needs a human + NVDA/VoiceOver against the running Aspire app.
Defect-fix wave, 2026-07-05 (A-1..A-7, cross-repo defect audit)
Targeted correctness wave; every behavior change flipped its pinning test in the same commit.
- A-1 Cancellation no longer swallowed:
AnthropicScoringService.ScoreSessionAsyncand theScoreEventSessionsHandlerpersistence catch now filterwhen (ex is not OperationCanceledException)(repo idiom, cf.UserRegisteredHandler); the service's "never throws" doc is scoped to scoring failures, cancellation propagates. - A-2 Partial score JSON rejected: the seven
AiScoreResponsesub-scores are nullable; any missing one returns the failed-result shape instead of defaulting to 0 and clamping up to 1.0. Out-of-range clamping for present values is unchanged;reasoningstays optional. - A-3 (doc-only) Speaker-overlap docs corrected:
GetSpeakerSessionOverlapHandler,SpeakerSessionOverlapDTO/MultiSessionSpeakerdocs, and the pinning-test comment now state the handler intentionally returns EVERY speaker with a submitted session (the UI shows all speakers with a session-count column), sorted so multi-session speakers surface first. No behavior change; types not renamed. - A-4 Category-distribution soft-delete drift fixed:
GetCategoryDistributionHandler's category-existence predicate aligned withGetSessionSelectionDashboardHandler(!c.IsDeletedplus live-item count check), so a category whose only referenced item is soft-deleted is omitted entirely. - A-5 Duplicate guards added:
Session.AddSessionCategoryItemandSpeaker.AddSpeakerCategoryItemnow reject a live duplicate association (codesSession.CategoryItem.Duplicate/Speaker.CategoryItem.Duplicate), mirroringAddSessionSpeaker; re-add after soft-delete still succeeds. Verified both Sessionize sync strategies pre-filter live duplicates before calling Add, so re-imports are unaffected. - A-6 GDPR role check case-sensitivity (mirror of the Store fix):
DeleteUserHandler/ExportUserDataHandlercompared the raw role claim string ordinally againstUserRole.Organizer(via the implicit string conversion), denying organizers whose claim carried different casing. New case-insensitiveUserRole.IsOrganizer(string?)helper used in both, with lowercase-claim regression tests. - A-7 (cosmetic):
SessionLookupServicedropped the misleadingpageSize=10000query param: the base/sessionsendpoint has no pageSize parameter and always serves one page capped at MaxPageSize (500), so this was a verified non-bug (comment added noting the cap);SpeakerDashboardServicenotes the same cap; the staleMMCA.ADC.slnxcomment claiming the deleted combinedMMCA.ADC.IntegrationTestsproject "stays excluded pending re-home" was corrected (the folder is gone; the per-service projects are the integration tier).
Current top levers (2026-07-03, after the e2e-gate promotion and the sixteenth-cycle full re-score): the former Blazor-Server-under-load blocker is resolved for the gate itself (the E2E_FORCE_SERVER pin + reload-and-rewait fixes; chromium E2E/axe now gates every deploy, #28 closed, TD-06/TD-07 done). The open set is now priority 3: #18, #19, #21 and priority 2: #12, #13, #22, #23, #33. The cheapest win remains the recorded manual screen-reader pass (#21 maturity 3→4, needs a human). The one structural play is the paired §18 + §19 UI fitness gates (one arch-test wave reaches Optimized/M4 on both, subsuming TD-13). #22 waits on a reliably green firefox/webkit soak before gating the non-chromium legs. New this cycle: TD-14 under #24 (forms error-presentation substance, the §24 impl 7→8/9 lever).
Update 2026-07-06 (eighteenth-cycle full re-score, pin v1.106.0): no scores moved. TD-03 closed (#8 optimistic-concurrency round-trip now implemented and deploy-gated, Conference-only so §8 holds impl 9) and TD-02 partially addressed (the genuine broker round-trip test landed as the non-gating MMCA.ADC.CrossService.IntegrationTests; gating it plus enabling the inbox on all 4 services is the §6 impl 9→10 lever). The open maturity-3 set (§12/§13/§18/§19/§21/§22/§23/§33) is unchanged, and the cheapest win remains the recorded manual screen-reader pass (#21 maturity 3→4). Evidence counts refreshed (arch-tests 23/25/74, §14 unit 1507/223 + integration 303 gating / four tiers + 9 non-gating CrossService, coverage floor 38→55.5%, ADR set 001-038, §27 resx 40+40).
Correction 2026-07-17: a paragraph formerly here, labeled "Update 2026-07-12 (twentieth-cycle full re-score, pin v1.115.0, HEAD 0c9507b8)", was a stale draft from the superseded nineteenth-cycle working diff, accidentally committed via PR #15 (whose subject was the §31 Log Analytics ingestion cap). The actual twentieth-cycle re-score is 2026-07-15 / pin v1.116.0 (recorded in the Index note above); it did NOT close #12 (the §12 maturity candidacy was rejected and §12 held M3/I8), and its arch-test/ADR counts differed from the draft's. The same stale hunk had also overwritten the #12 header ("RESOLVED M4/I8") and two scorecard prose blocks (a "§12 mat 4" strength claim and a risk-1 rewrite asserting the firefox/webkit e2e-gate legs cannot fail the deploy, describing the pre-2026-07-16 e2e.yml); all are corrected in this cycle's pass.
Update 2026-07-17 (twenty-first-cycle full re-score, pin v1.117.0, HEAD c4c01aa5): two scores moved up, both maturity, on the 2026-07-16 gates. #13 and #22 closed to maturity 4 (protect set now 31): #13 on the ADC-local ObservabilityConventionTests alert-runbook pairing gate in the CI.slnf arch gate, #22 on the fully gating three-browser e2e-gate (e2e.yml:78 scopes continue-on-error to scheduled nightly non-chromium legs). The open below-4 set shrank to §12/§21/§33: #21 (the recorded manual screen-reader pass remains the cheapest maturity 3→4 win, needs a human + NVDA/VoiceOver; the Warning-outlined-alert AA-contrast item was FIXED 2026-07-16), #12 (M3/I8 re-confirmed: the k6 tier executes monthly/dispatch out of band; Notification pinned maxReplicas: 1), #33 (M3/I8 re-confirmed; the Service Bus emulator tier candidacy stands for a future cycle). One candidacy adversarially rejected as a verified non-move: #27 impl 8→9 (pseudo-loc coverage is 3 public pages of 30+, partial). Evidence counts refreshed (arch-tests 26 classes / 28 files / 82 methods, 3 ADC-local, re-run green this cycle; ADR set 001-048, pin v1.117.0, 15 packages; indices Maturity 97.8% (313/320) / Implementation 86.3% (690/800)).
Update 2026-07-21 (twenty-second-cycle full re-score, pin v1.121.0, HEAD 8509a05d): two scores moved down, neither on a code-quality regression, and the protect set drops to 30. #22 REOPENED at M3/I8 (priority (4-3)x2=2): the 2026-07-18 Actions-minute reduction cut the deploy e2e-gate to browsers: '["chromium"]' (deploy.yml:478-480,488), so firefox/webkit run only on the weeknight nightly matrix where e2e.yml:119 keeps them continue-on-error. The cheapest recovery is option (b) under #22: a cross-browser-freshness job in deploy.needs mirroring the dr / load / cross-service pattern (deploy.yml:496,553,610), which restores an enforced signal at near-zero runner minutes. #18 implementation 9→8 with maturity held at 4, tracked as new TD-16 (effort S): HappeningNow.razor.cs is flush at the enforced 400-line cap with zero headroom and six more files sit 360-379, so sub-component extraction per the TD-13 pattern is the impl 8→9 lever. Open below-4 set: §12/§21/§22/§33. #21 (the recorded manual screen-reader pass, still the cheapest maturity 3→4 win, needs a human + NVDA/VoiceOver) remains the highest-priority item at 3; #12/#22/#33 sit at priority 2. #12 banked its Redis-provisioning sub-item (infra/main.bicep:740) but stays open on the maxReplicas: 1 Notification pin (:1424); #33 keeps its score on a rewritten basis after its README.md:74 quote was found deleted. One candidacy rejected for a second cycle: #27 impl 8→9 (pseudo-loc covers 3 public pages of 36 routable). Indices Maturity 97.2% (311/320) / Implementation 85.9% (687/800), pin v1.121.0, ADR set 001-050.
Update 2026-07-23 (twenty-third-cycle full re-score, pin v1.123.0, HEAD 160f59f5): no scores moved and the ledger is unchanged: no closures, no new items, no re-ranks, and every TD sub-item status holds. The open below-4 set stays §12/§21/§22/§33 (#21 at priority 3, #12/#22/#33 at priority 2). Two proposed maturity lifts were adversarially rejected as verified non-moves: #12 (the k6 tier runs monthly/dispatch out of band, load-test.yml:8; load-freshness is a recency-only deploy check, deploy.yml: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.