Architecture governance
MMCA.ADC: Architecture Scorecard
Canonical, version-controlled scorecard for this repo: the single source of truth for MMCA.ADC's architecture scores, scored against the rubric at
ArchitectureEvaluationCriteria.md; framework-wide facts in../MMCA.Common/FACTS.md. Remediation lives inRemediationBacklog.md, the single remediation and tech-debt ledger (it carries the tacticalTD-NNitems); the cross-repo comparison lives in the workspace-internalDocs/Architecture/CrossRepoComparison.md(not published).
Evidence as of 2026-10-18 at MMCA.Common.* v1.244.0, git HEAD cba3a433 (clean tree). Rubric version 2.
At a glance
- Maturity index = Σ(maturity×weight) ÷ Σ(weight×4) = 314 ÷ 324 = 96.9%
- Implementation index = Σ(impl×weight) ÷ Σ(weight×10) = 697 ÷ 810 = 86.0%
- Weaker axis: Implementation, 10.9 points below Maturity.
- Maturity band: 4 categories sit below maturity 4 (Sections 10, 12, 21 and 22; 10 weighted points), ranked in the backlog.
- Implementation band: 14 categories sit at implementation 8 or below (34 weighted points short of 9 on every category), ranked in the backlog.
MMCA.ADC is a .NET 10.0 DDD / Clean Architecture system for the Atlanta Developers Conference, run as four independently hosted services (Identity, Conference, Engagement, Notification) behind a YARP gateway, collaborating over Result-over-the-wire gRPC and MassTransit integration events on the outbox, one database per service, on the lockstep MMCA.Common.* packages (19 of the 22 published packages, Directory.Packages.props:98-130). Its strongest trait is that architectural intent is executable: 54 architecture-test classes gate CI on rules inherited from the shared MMCA.Common.Testing.Architecture library, and recovery, capacity, cost and cross-browser proofs are deploy preconditions sized to measured conference load (~67 peak). The remaining weight sits on the implementation axis, led by the responsive substance of §22 (implPriority 4), and on four maturity gaps: §10's broker faults have no alert or replay procedure, §12's capacity proof runs out of band by decision, §21's manual screen-reader pass is unrecorded, and §22's phone-width layout is guarded by review rather than by a test.
Top 5 risks
Responsive behavior holds the lowest implementation score and sits in both bands: §22 (mat 3 / impl 7, weight 2; the top row of the implementation band at implPriority 4, and in the maturity band at priority 2). Cross-engine recency is gated (
cross-browser-freshness,deploy.yml:923, indeploy.needs:1159, hard condition:1193), but that is the only automated check: no ADC test sets a phone viewport and no responsive fitness test exists, so layout, reflow and density are enforced by review. Both implementation deductions hold: the rubric's density-options criterion has zero adoption in ADC although the framework ships it (DataGridListPageBase.cs:78DenseGrid,:909ToggleDensity();EventList.razor:73hardcodesDense="true"), and 20.razorfiles render raw tables outside the DataGrid's grid-to-card degradation; 12 of them are key/value detail tables that wrap on mobile (adc-ui.css:33), while the session-selection statistics tables use fixed-width columns with no mobile alternative (adc-ui.css:80).- Remediation: Adopt the density toggle on the DataGrid list pages (bind
DenseGrid/ToggleDensity()instead of the hardcodedDense), give the selection-dashboard and organizer tables a card or stacked degradation at the phone breakpoint, and add an E2E or fitness check that asserts layout at a phone viewport (effort M). - Expected: An automated phone-viewport check lifts §22 maturity 3→4; density adoption lifts impl 7→8, and reflow on every raw-table surface lifts it 8→9.
- Remediation: Adopt the density toggle on the DataGrid list pages (bind
The manual screen-reader pass is unrecorded: §21 (mat 3 / impl 8, weight 3). The chromium E2E/axe suite blocks the deploy (
e2e-gateindeploy.needs), cross-browser recency is gated (cross-browser-freshness,deploy.yml:923, indeploy.needs:1159), and one live-moderation dialog carries a keyboard-operability test (LiveModerationKeyboardTests.cs:23, a subclass of Common'sKeyboardOperabilityTestsBase), but the recorded screen-reader pass that the rubric's maturity-4 bar asks for is still the empty placeholder row inadc-ACCESSIBILITY-SCREENREADER-PASS.md:64.- Remediation: Record the manual screen-reader pass (needs a human with NVDA or VoiceOver).
- Expected: §21 maturity 3→4 (weight 3).
Broker faults have no operational path: §10 (mat 3 / impl 8, weight 3).
FaultIntegrationEventConsumer.cs:57(Common) logs "faulted ... moved to the error queue", but the only messaging log alert matches the outbox's "dead-lettered" text (infra/main.bicep:605), and its runbook section covers outbox rows only (infra/OPERATIONS.md:188, manual replay by a row reset at:219), so no alert or runbook covers broker faults; ADR-087 records no alert, runbook or replay path (../adr/087-broker-poison-message-handling.md:202-204). Every integration event must declare[EventName](CommonEventConventionTestsBase.cs:21, inherited byContracts/EventConventionTests.cs:3), and the contract snapshot spells out generic arguments and is keyed by that wire identity (CommonArchitectureRules.Events.cs:45,:82;IntegrationEventContractTests.cs:17). The missing ordering stamp on the speaker-link handlers is an accepted limit recorded in the code (SpeakerLinkedToUserHandler.cs:25-27).- Remediation: An alert plus runbook for the broker error queue with a replay procedure, wired as alert-as-code.
- Expected: The alert, runbook and replay procedure lift §10 impl 8→9; alert-as-code for broker faults is the maturity 3→4 lever.
SQL data plane: public network access remains: §11 (mat 4 / impl 8, weight 3) and §17 (mat 4 / impl 9). App-to-database auth is passwordless managed identity in production, with the SQL admin login kept only as a dormant fallback. The residual is network exposure:
infra/main.bicep:861publicNetworkAccess: 'Enabled'plus the AllowAzureServices firewall rule at:867, both deferred by design (no VNet; documented-accepted ininfra/SQL-MANAGED-IDENTITY.md). §11's other v2 deduction, the missing threat model, is a recorded decision not to schedule it.- Remediation: Move the SQL data plane onto private endpoints (disable public network access, drop the 0.0.0.0 rule). This is the VNet and private-endpoint epic, which requires recreating the Container Apps environment, hence deferred.
- Expected: Closing public network access is the 9→10 rung for §17; §11 holds at 8 until a lever other than the unscheduled threat model is named.
The API breaking-change detector cannot see parameters and the MAUI head sits outside CI: §9 (mat 4 / impl 8) and §15 (mat 4 / impl 8). All four services diff their live OpenAPI document against a committed
Contract/openapi-baseline.json(ConferenceOpenApiContractTests.cs:29, Identity:39, Engagement:30, Notification:31), and the shared base fails on any detected breaking change (MMCA.Common.Testing/Conformance/OpenApiContractTestsBase.cs:118-159), but the detector compares only request bodies and responses, never operation parameters (OpenApiBreakingChangeDetector.cs:190-216), so a removed, renamed or newly required query or path parameter passes; a request body is compared only when both sides have one (:192-193), and nothing ratchets the baseline against the committed build documents (Source/Services/*/openapi/*.json), so a route added after seeding is unprotected until the baseline is refreshed by hand. The async contract has no published document by decision (the frozen event snapshot guards the seven events; see the backlog's Deliberate and accepted). The MAUIMMCA.ADC.UIproject is in no CI build (MMCA.ADC.CI.slnf:25,:26list only the web heads), so its analyzers and its ownNoWarn CA5392(MMCA.ADC.UI.csproj:155) are review-only.- Remediation: Compare operation parameters (and request-body presence) in the Common
OpenApiBreakingChangeDetector, then ratchet each ADC baseline to its committed build document; bring the MAUI head inside a CI build (TD-18), verified by a full-solution package-mode restore. - Expected: The parameter-aware, ratcheting detector closes the HTTP half of the §9 contract-test criterion; MAUI inside a CI build lifts §15 impl 8→9.
- Remediation: Compare operation parameters (and request-body presence) in the Common
Top 5 strengths
Architecture intent is executable, not prose (§34 Governance, mat 4 / impl 9, and §3 Clean Architecture, mat 4 / impl 9): 54 architecture-test classes in
Tests/Architecture/MMCA.ADC.Architecture.Tests/gate CI (LayerDependencyTests,DomainPurityTests,MicroserviceExtractionTests, plus the PII, concurrency, integration-event contract, specification, slice-cohesion, data-residency, constructor-dependency, brand-token, inline-style, cost-tag, SQL-audit, UI-architecture, state-management and observability-pairing gates), with most rule bodies inherited from the sharedMMCA.Common.Testing.Architecturebases (ADR-015) and 14 ADC-local[Fact]/[Theory]methods (for exampleUi/InlineStyleTests.cs:21,Governance/CostTagConventionTests.cs:20,:39,Ui/FormsConventionTests.cs:69,:108); the ADR set (range and count owned by../adr/README.md) captures the why.- Remediation: Refresh the workspace architecture map (
Docs/Architecture/ArchitecturalAnalysis.md, versioned in the private harness repo), which still states 13 packages at v1.82.0 (:99). - Expected: §34 impl 9→10.
- Remediation: Refresh the workspace architecture map (
Disaster recovery is drilled and gated, graceful shutdown is CI-verified, and the privacy residency claim matches the deployment (§29 mat 4 / impl 9, §30 mat 4 / impl 9):
infra/DISASTER-RECOVERY.md:212-217records six PITR restore proofs across all four live databases (the newest restoresADC_Conferencein 2.1 min against the 2 h RTO); thedr-freshnessstep fails the deploy unless the latestdr-drill.ymlrun succeeded within an 8-day window (deploy.yml:844,window-daysat:853, indeploy.needsat:1159), and the drill job itself runs under a 60-minute timeout (dr-drill.yml:49, rationale:42-48);Tests/Hosts/MMCA.ADC.Gateway.Tests/GracefulShutdownTests.cs(a sealed subclass ofMMCA.Common.Testing/GracefulShutdownTestsBase.cs) CI-verifies a bounded drain;PRIVACY.md:53,61reads East US 2 / West US 2 and is guarded byDataResidencyTests.- Remediation: A recorded restore-RTO trend is the remaining polish.
- Expected: Polish only; no score move is scheduled.
Reference-quality front-end security and UI state management (§26 mat 4 / impl 9, §19 mat 4 / impl 9):
Hosts/UI/MMCA.ADC.UI.Web/Program.cs:103(AddSessionCookieAuthentication()),:177(AddCommonBlazorCsp(), the CSP provider) and:198(UseCommonSecurityHeaders()), all consumed from MMCA.Common: HttpOnly-cookie tokens, refresh unreadable by JS, no localStorage, locked script-src/connect-src, frame-ancestors none;ListPageStateService.cs:9,81(per-circuit scoped, immutable records) andUnsavedChangesGuard.razor:32(IsDirtyAccessor pre-empts the stale-IsDirty red flag), with zero mutable static cross-user state machine-proven by the CI-gatingStateManagementConventionTests.- Remediation: Replace
style-src 'unsafe-inline'with the framework's nonce path and narrowimg-src https:to specific origins. - Expected: §26 impl 9→10.
- Remediation: Replace
Capacity and FinOps are data-driven from measured conference load (§31 mat 4 / impl 8, §12 mat 3 / impl 9):
conference-read-load.js:56-57(p95<800 ms, <1% errors; 67 VUs, the observed peak, on manual dispatch,load-test.yml:18, and 40 VUs on the scheduled run,:75), a green proof on the deployed 0.25 vCPU / 0.5 Gi topology (run 35013811738,infra/main.bicep:182) whose recency gates the deploy (load-freshness,deploy.yml:863);cost-guard.ymlindeploy.needs(deploy.yml:783,:1159, condition:1192) fails the deploy if maxReplicas>2 or any SQL DB tier!=Basic (cost-guard.yml:59-61,:74-76); a monthly RG budget with 80%/100% alerts (main.bicep:819); a per-serviceservicecost tag on the six container apps and the four databases (main.bicep:1705,:991); a 5 GB/day Log Analytics ingestion cap sized as a runaway guard (foundation.bicep:15, applied at:53-55).- Remediation: Run the read-only
cost-guarddaily instead of weekly (cost-guard.yml:14, Mondays only), so an expired surge whose revert deploy has not run is caught within a day. - Expected: §31 impl 8→9.
- Remediation: Run the read-only
Non-inverted test pyramid with deploy-gated integration, E2E and markup-snapshot tiers (§14 mat 4 / impl 9, §28 mat 4 / impl 9): 1507 unit/UI
[Fact]/[Theory]across 223 files; 303 gating integration methods across four per-service WAF tiers; 12 markup-snapshot[Fact]s over 12 committed.htmlbaselines in all three bUnit projects (ComponentsSnapshotTests.cs:27), failing on a missing baseline. The integration tier is a required PR check, the coverage floor gatesbuild-and-test(deploy.yml:406), and branch protection enforces admins with strict up-to-date required checks (ADC PR #258), so the merged tree is the treebuild-and-testran on (rationale atdeploy.yml:812-816) and no code deploy reaches production with zero test execution, including the backend-only deploys the UI-scopede2e-gateskips.- Remediation: For §28's 9→10 rung: pixel visual regression, a wider snapshot slice than 12 components, and a browser tier that does not skip on backend-only deploys (the UI-scoping is recorded as a deliberate Actions-minute trade-off under the backlog's Deliberate and accepted).
- Expected: §28 impl 9→10.
Scorecard
| # | Category | Weight | Maturity (0-4) | Impl (0-10) | Weighted (M·w / I·w) | Evidence / Notes |
|---|---|---|---|---|---|---|
| 1 | SOLID Principles | 3 | 4 | 9 | 12/27 | SRP/OCP/DIP exemplary and machine-enforced: tiny cohesive handlers, Strategy for variation, abstraction-only injection (including TimeProvider over ambient DateTime.UtcNow in RefreshFromSessionizeHandler/AuthenticationService), NetArchTest layer rules gated in CI; narrow ISP interfaces; an LSP fitness test fails the build on any override or interface implementation that throws NotSupportedException/NotImplementedException (Tests/Architecture/MMCA.ADC.Architecture.Tests/Governance/SubstitutabilityTests.cs:9, a subclass of the shared base). Minor LSP/ISP nuances (a few Delete() overrides) keep it off 10. Evidence: Conference.Application/Events/UseCases/Publish/PublishEventHandler.cs:14-15 (ctor = IUnitOfWork+ILogger only); Conference.API/Controllers/EventQuestionAnswersController.cs:56-63 (injects handlers/IEntityQueryService<>, never repos); .../RefreshFromSessionize/{ISessionizeSyncStrategy,SpeakerSyncStrategy}.cs (per-entity Strategy, no growing switch); a ctor-dependency-ceiling fitness function Tests/Architecture/MMCA.ADC.Architecture.Tests/Cqrs/ConstructorDependencyCountTests.cs (service ceiling 8 with AuthenticationService at the 8 high-water mark, :31; controller ceiling 10, :59; handler ceiling 7, :68) |
| 2 | Design Patterns | 2 | 4 | 9 | 8/18 | Creational (entity factories/Options), structural (decorator pipeline, manual mapper adapter, gRPC adapters), behavioral (Strategy, dispatcher, Specification, domain events), and domain patterns (Result, Repository/UoW, Outbox) all present, idiomatic, ADR-justified, consistently named. Evidence: Identity.Domain/Users/User.cs:226 (Factory Create→Result<User>); Conference.Application/Events/Specifications/PublishedEventSpecification.cs:11,14 (read scoping: the BR-108 published-only criteria applied for non-organizer readers on public endpoints); exactly 5× {Category,Room,Speaker,Session,Question}SyncStrategy.cs |
| 3 | Clean Architecture | 3 | 4 | 9 | 12/27 | Dependency rule inward-pointing, domain framework-pure, ports in Application/adapters in Infrastructure, rules enforced automatically by NetArchTest gated in CI: textbook; only the rich per-layer project sprawl keeps it shy of 10. Evidence: Tests/Architecture/MMCA.ADC.Architecture.Tests/{LayerDependencyTests,DomainPurityTests,MicroserviceExtractionTests}.cs (thin sealed subclasses of the shared MMCA.Common.Testing.Architecture rule library, ADR-015, Domain free of App/Infra/API references and App free of Infra/API references per module, domain framework-purity, transport-at-edge; all in MMCA.ADC.CI.slnf) |
| 4 | Domain-Driven Design | 3 | 4 | 9 | 12/27 | Rich Result-returning factories with combined invariants (User.cs:167), domain events raised by the aggregates (User.cs:307, Event.cs:222,:292), child collections encapsulated behind IReadOnlyCollection (Event.cs:92,:98,:112), and the newest aggregate exemplary (alias-typed ID references only, invariants, Result factory, event in the same transaction: Engagement.Domain/CheckIns/CheckIn.cs:89). MMCA.ADC.Conference.Domain has zero public setters (get; set; } returns no match), and the object navigations beside the by-ID references are private-set (Session.cs:71,:75, Activity.cs:58, Sponsor.cs:49). Event.OrganizerContactEmail is the shared Email value object (public Email? OrganizerContactEmail { get; private set; }, Event.cs:59, private constructor :121, the same shape as Speaker.cs:133,:211), and Event.Create (:170, Email.Create at :189) and Event.Update (:247, :265) return its invariant failure before touching the aggregate. Tenancy is met by decision: ADR-073 records ADC as single-tenant (073-multi-tenancy-model.md:185), and no AddMultiTenancy appears in ADC source. Held below 10: SponsorshipPacketUrl and TicketingUrl are assigned unchecked (:289-290; the Result.Combine at :195 and :271 validates only name/timezone/date range) while their absolute-URL and length rules live in the Application layer as EventSponsorshipPacketUrlRules / EventTicketingUrlRules (EventValidationRules.cs:77,:97, composed at :149-150), against the repo's own Sponsor.Create convention (Sponsor.cs:119); the private-set object navigations remain. Maturity 4: Tests/Architecture/MMCA.ADC.Architecture.Tests/Domain/EntityConventionTests.cs:3 + Domain/ImmutabilityTests.cs:3 (sealed subclasses of the shared bases; EntityConventionTestsBase.cs:18 enforces Result-returning factories, no public aggregate constructors, sealed Domain-resident entities and non-public setters) over AdcArchitectureMap.cs:28,:36,:44 (Identity, Conference, Engagement; Notification ships no Domain assembly by design, :9), gating CI via MMCA.ADC.CI.slnf:58 + deploy.yml:366 |
| 5 | Vertical Slice Architecture | 2 | 4 | 8 | 8/16 | Within Application, features are genuine cohesive slices (command+validator+handler+DTO together, low inter-slice coupling, pipeline-handled cross-cutting), and slice cohesion is machine-enforced: ADC subclasses the shared SliceCohesionTestsBase fitness function (MMCA.Common.Testing.Architecture), which fails the build if a handler/validator is stranded from its same-assembly contract (ArchitectureRules.Slices.cs:13,31), over all four modules (AdcArchitectureMap.cs:12-54), in MMCA.ADC.CI.slnf so it gates every push/PR (the rubric's M4 "enforced automatically by tests/CI"). Held at 8: the solution is deliberately layered-by-project Clean Architecture, so a feature spreads Domain/Application/Infrastructure/API across assemblies (a conscious hybrid), and a new use case edits the per-aggregate controller plus the module's EF configuration rather than only adding a slice; the rubric's first §5 criterion also wants the DTO inside the slice, while ADC's DTOs live in the Shared assembly (Conference.Shared/Events/EventDTO.cs) with their mappers in a horizontal Application/Events/DTOs/, beside horizontal Validation/, Specifications/ and DomainEventHandlers/ folders. Two cross-slice internal calls (RemoveUserAvatarHandler into SetUserAvatarHandler.TryGetBlobName, GetNowNextHandler into CalendarExportMapper) are a rubric red flag that rules out the 9 band. Evidence: Tests/Architecture/MMCA.ADC.Architecture.Tests/SliceCohesionTests.cs:8; MMCA.ADC.CI.slnf:58; deploy.yml:366 (the arch gate executes the filter); ArchitectureRules.Slices.cs:13,31; AdcArchitectureMap.cs:12-54; Conference.Application/Events/UseCases/AddEventQuestionAnswer/; Application/{Aggregate}/UseCases/{Operation}/ uniform across modules |
| 6 | CQRS & Event-Driven | 2 | 4 | 9 | 8/18 | Commands (Result, ITransactional) and queries cleanly separated via a documented, enforced decorator pipeline; outbox gives atomic persist-then-publish with smart-wait/retry/dead-letter; the integration-event wire contract is frozen by a CI fitness function; events carry a Common-supplied SchemaVersion. Consumer idempotency is infrastructure-backed (EfInboxStore dedup) via EnableInbox=true on all four services (Conference/Identity/Engagement/Notification, appsettings.json). Held at 9, not 10: the versioning path is present-but-unexercised (SchemaVersion is virtual ... => 1, no event has a v2), and the genuine broker round-trip covered by MMCA.ADC.CrossService.IntegrationTests runs only in a non-gating nightly (cross-service-tests.yml) whose recency is enforced by a cross-service-freshness deploy check rather than the tier itself gating. Evidence: Tests/Architecture/MMCA.ADC.Architecture.Tests/IntegrationEventContractTests.cs:9-14 (frozen contract, CI-gated via MMCA.ADC.CI.slnf:58); EnableInbox=true on all four service appsettings.json (Identity/Conference/Engagement/Notification); Common BaseIntegrationEvent.cs:22 (SchemaVersion virtual, default 1); Common OutboxProcessor.cs; ADR-003 |
| 7 | Microservices Readiness | 3 | 4 | 8 | 12/24 | Service boundaries align with bounded contexts, each owns its own DB (no shared writable schema), integration async-via-outbox + sync-via-versioned-gRPC, resilience a CI-enforced invariant, transport-at-edge keeps modules extractable/reversible, and the enforced map covers all four services (AdcArchitectureMap.cs:51-54). Held at 8: the synchronous call graph has eight typed gRPC client registrations across all four services (Conference.Service/Program.cs:392, Engagement.Service/Program.cs:281, Identity.Service/Program.cs:316, Notification.Service/Program.cs:225) with two bidirectional pairs, Conference↔Engagement and Identity↔Notification, plus a three-node Conference→Engagement→Notification→Conference call loop, over seven .proto contracts frozen by the snapshot fitness test (documented, self-healing via the resilience pipeline; the AppHost deliberately omits the reciprocal WaitFor on four edges, AppHost/Program.cs:259). That is the rubric's named red flag (ArchitectureEvaluationCriteria.md:266). The rubric v2 "modernization patterns named" criterion (:263) is met in the ADRs: each *.Contracts gRPC adapter is named the module's Anti-Corruption Layer (../adr/007-grpc-extraction.md:26) and extraction follows the Strangler Fig route (../adr/008-service-extraction-topology.md:52). The Conference→Engagement half of the bidirectional pair, the bookmark-count read, is one batched call cached 60 s with broker-driven eviction (Conference.Service/Program.cs:294-301) and is recorded as accepted in the backlog. Evidence: ADR-008/006/007; ProtoContractTests.cs:9 (seven frozen protos); Tests/Architecture/MMCA.ADC.Architecture.Tests/MicroserviceExtractionTests.cs (transport-at-edge guard, thin subclass of the shared rule library) |
| 8 | Data Architecture | 3 | 4 | 9 | 12/27 | Transaction boundaries match aggregates (UoW + per-source best-effort), soft-delete + audit + RowVersion concurrency applied centrally by convention and locked by a build-time fitness function, migrations versioned/idempotent with a model-drift CI gate, per-service isolation + navigation populators address cross-context joins and N+1. SessionIncludeChildrenRegressionTests.cs:27 proves GET /sessions?includeChildren populates sessionSpeakers, and the E2E-level SpeakerSelfServiceTests.cs second journey is an active [Fact] at :81. ADC opts into Common's cross-source SpecificationConventionTests (the polyglot safety gate, ADR-018) while routing all entities to SQL Server with the framework extension points kept (every appsettings Cosmos/SQLite connection string is empty; AppHost uses WithSQLServerDataSource for all four modules). The optimistic-concurrency round-trip is implemented and deploy-gated across Conference and Engagement (If-Match conditional endpoints on LivePoll open/close, Engagement.API/Controllers/LivePollsController.cs:87, and on question approve/dismiss/mark-answered, SessionQuestionsController.cs:134, with the RowVersion override in OpenLivePollHandler.cs:35); Identity exposes none. Held at 9 by decision (a proposed 9→8 on the analytical-separation criterion is not taken): the rubric v2 criterion "analytical reads separated from OLTP" (ArchitectureEvaluationCriteria.md:289) is unmet, the CSV export streaming off the transactional store under ADR-078's page loop and row cap (../adr/078-csv-export-endpoint.md:71) with no replica, CDC or recorded no-analytics decision, which is the named 9→10 lever; and SessionScoringRunner.cs:104 issues ExecuteDeleteAsync outside the SaveChangesAsync unit of work (mitigated by a unique filtered index). Tenant isolation is decided by ADR: ADC is single-tenant (../adr/073-multi-tenancy-model.md:191). Evidence: Conference.Shared/Events/EventDTO.cs:16,52 (RowVersion token via IConcurrencyAware), Conference.Application/Events/UseCases/Update/UpdateEventHandler.cs:34 (the RowVersion(...) base-class override), Tests/Integration/MMCA.ADC.Conference.IntegrationTests/Organizer/OrganizerConcurrencyTests.cs:44 (stale token→412 Precondition Failed, missing token→428, ADR-035, in the deploy-gating Integration.slnf); deploy.yml:234-282 (expand/contract migration guard, fails closed, in the required build-and-test job listed at CONTRIBUTING.md:38), :450 (EF model-drift gate); Common ApplicationDbContext.cs:189 (audit-stamping SaveChangesAsync), :448 (ApplySoftDeleteFilters), :582 (ConfigureConcurrencyTokens); ConcurrencyConventionTests.cs; SpecificationConventionTests.cs (cross-source gate, active) |
| 9 | API & Contract Design | 2 | 4 | 8 | 8/16 | Consistent resource shapes via the shared base controller, header-based versioning (api-version, ReportApiVersions set framework-side at MMCA.Common/.../Startup/WebApplicationBuilderExtensions.cs:253) on all 39 module controllers (Conference 23, Identity 10, Engagement 6), standardized RFC 9457 errors guarded by ProblemDetailsContractTestsBase.cs:74, edge validation, manual DTO decoupling (ADR), uniform pagination/sorting/filtering (X-Pagination emitted by SpeakersController.cs:206 and SessionsController.cs:208), OpenAPI served per service at /openapi/v1.json, four committed per-service OpenAPI documents behind a drift gate (deploy.yml:305-320), and a real deprecated-v1.0 / v2.0 pair on ServiceInfoController.cs:18-19 proven by the CI-gated ApiVersioningTests.cs:14. Breaking-change baseline: all four services opt into the shared base's baseline diff against a committed Contract/openapi-baseline.json (Conference Tests/Integration/MMCA.ADC.Conference.IntegrationTests/Contract/OpenApiContractTests.cs:29, Identity :39, Engagement :30, Notification :31; opt-in documented at OpenApiContractTestsBase.cs:21-28, used at :118), and the base fails on any detected breaking change (:118-159: removed path, operation or response status, newly required request property, removed response property, changed type), inside the required integration-tests check (deploy.yml:641-729). Held at 8 on the rubric v2 criterion "contract tests at the boundary" (ArchitectureEvaluationCriteria.md:316): the detector compares only request bodies and responses, never operation parameters (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/OpenApiBreakingChangeDetector.cs:190-216; the Conference baseline carries 159 parameters blocks), so a removed, renamed or newly required query or path parameter, or a parameter type change, passes; a request body is compared only when both sides have one (:192-193), a removed media type passes (:226-230), and enum, composition and format changes are not classified; the baseline does not ratchet, since nothing keeps openapi-baseline.json in step with Source/Services/*/openapi/*.json, so a route added after seeding is unprotected until the baseline is refreshed by hand; and the async half of the criterion has no AsyncAPI or equivalent published document, the seven integration events being frozen byte-exactly inside Tests/Architecture/.../Contracts/IntegrationEventContractTests.cs:9, which the backlog records as Deliberate and accepted (no external event consumers). Lever: parameter-aware, ratcheting breaking-change detection (Common first). Maturity 4: the discipline is machine-enforced inside two server-required merge gates, integration-tests running MMCA.ADC.Integration.slnf:5 (CONTRIBUTING.md:40, branch-protection context at :97) and build-and-test running the arch tier (MMCA.ADC.CI.slnf:58, deploy.yml:366). Evidence: EventsController.cs:45 (ApiVersion), :101 ([Range]) |
| 10 | Messaging & Integration Architecture | 3 | 3 | 8 | 9/24 | Strong substance: the inbox dedups at-least-once delivery and is on by default for a broker (Common IntegrationEventConsumer.cs:81), retries use exponential backoff bounded at 5 (Common DependencyInjection.Messaging.cs:269), events carry versioned wire names (Identity.Shared/Users/IntegrationEvents/UserRegistered.cs:35), a frozen contract snapshot gates CI, the Service Bus emulator parity tier gates deploys through cross-service-freshness (.github/workflows/cross-service-tests.yml:129), gateway route-map tests are in MMCA.ADC.CI.slnf:60, and outbox dead-letter retention is 30 days in production (infra/main.bicep:1742). Cross-cutting concerns (validation, logging, transactions, caching, resilience, mapping, config) are centralized via decorators, shared middleware and typed startup-validated options (Common Application/DependencyInjection.cs:89-106, Common Infrastructure/DependencyInjection.cs:60-246; cache eviction guarded by an IsFailure early-return, EventsController.cs:179-268; ADR-001). Why Implementation 8: (1) the dead-letter criterion asks for an operational procedure (alert, inspect, replay or discard, ArchitectureEvaluationCriteria.md:336) and the consumer side has none: FaultIntegrationEventConsumer.cs:57 logs "faulted ... moved to the error queue" while the only messaging log alert matches "dead-lettered", the outbox text (infra/main.bicep:603-605), so no alert covers broker faults or broker.fault.count; ADR-087 records no alert, runbook or replay path (../adr/087-broker-poison-message-handling.md:202-204) and the dead-letter runbook (infra/OPERATIONS.md:188) replays only an outbox row reset (:219); (2) no compensating saga or reconciliation sweep exists (../adr/054-saga-compensation-and-reconciliation.md:172). The frozen contract snapshot spells out generic arguments (SessionId:Nullable<Int32>, Tests/Architecture/MMCA.ADC.Architecture.Tests/Contracts/IntegrationEventContractTests.cs:17) and is keyed by the [EventName] wire identity (Common ArchitectureRules.Events.cs:45,:82-89), every integration event must declare that name (Common Bases/Contracts/EventConventionTestsBase.cs:21, rule at ArchitectureRules.Events.cs:33-41, inherited by Tests/Architecture/MMCA.ADC.Architecture.Tests/Contracts/EventConventionTests.cs:3), and AGENTS.md:76 documents the emulator tier as authoritative. SpeakerLinkedToUserHandler.cs:59 overwrites LinkedSpeakerId with no ordering stamp, an accepted limit recorded at :25-27 (an organizer repairs it by re-linking), while SpeakerUnlinkedFromUserHandler.cs:56 guards the stale direction. Why Maturity 3: most criteria are CI-enforced, but broker faults are not wired to alert-as-code; M4 is also defensible, and M3 is the adjudicated score. Levers: an alert plus runbook for the broker error queue with a replay procedure (8 to 9), alert-as-code for broker faults (maturity 3 to 4) |
| 11 | Security | 3 | 4 | 8 | 12/24 | Authn/authz substance is exemplary: algorithm-pinned RS256/JWKS issuance with no shared key (Identity.Service/Program.cs:187, AddCommonAuthentication) and AddForwardedJwtBearer on the other three hosts; capability-based authz layered on roles via AddPermissions (Conference.API/DependencyInjection.cs:43) with a real ContentEditor subset (Conference.Shared/Authorization/ConferencePermissionGrants.cs:48) and [HasPermission] on the Conference/Identity endpoints; server-side owner-vs-Organizer scoping (Engagement.API/.../BookmarksController.cs); every prod secret in RBAC Key Vault read through the shared UAMI, ACR pulled without admin credentials; parameterized EF with the RawSql/RawQueryable/Pii convention tests. Maturity 4 on gate evidence: the allowlist anonymous-endpoint fitness function (Tests/Architecture/MMCA.ADC.Architecture.Tests/Api/AnonymousEndpointTests.cs:21,:33) and the Gateway header-regression project are in the CI filter (MMCA.ADC.CI.slnf:59,:60); the supply-chain job fails on any unsuppressed vulnerable package and on an empty CycloneDX SBOM and sits in deploy.needs (deploy.yml:608,:623,:1159); the authz integration matrices gate every PR. Why 8, not 9 (the rubric v2 criteria, ArchitectureEvaluationCriteria.md:365-367, ADR-110): (1) no threat model exists in the repo, an explicit rubric red flag (the nearest artifact is a workspace-internal, uncommittable point-in-time review), and committing one is deliberately not scheduled; (2) service-to-service authentication by identity holds on four of the seven gRPC surfaces (RequireAuthorization + JwtForwardingClientInterceptor: Conference.Service/ConferenceGrpcEndpoints.cs:20, Engagement.Service/Program.cs:355, Notification.Service/Program.cs:301, Identity.Service/Program.cs:373) but BookmarkCountsGrpcService (Engagement.Service/Program.cs:347, rationale Grpc/BookmarkCountsGrpcService.cs:12-21), LiveChannelGrpcService (Notification.Service/Program.cs:293, Grpc/LiveChannelGrpcService.cs:14) and EventLiveValidationGrpcService (Conference.Service/ConferenceGrpcEndpoints.cs:32, rationale :22-31, with "a fresh token at join" recorded as the follow-up at :30-31) are AllowAnonymous and trusted by network position, documented with bounded payloads and internal-only ingress: a documented partial; (3) a Google/FCM client API key is committed in Source/Hosts/UI/MMCA.ADC.UI/appsettings.json:34 (deliberate per Docs/MobileReleaseRunbook.md:101-103, a public client identifier, minor). Tenant-scoped authorization is met by statement: ADC is single-tenant (ADR-073). The 9-10 band needs every criterion met to a high standard, so one unmet and one partial place the category at the top of the Strong band. Beneath that, a cap deferred by design: managed-identity SQL auth is opt-in through the useManagedIdentitySql repo variable (bicep default false at infra/main.bicep:36, staged at :215-218 and runbooked in infra/SQL-MANAGED-IDENTITY.md; the sqlAdminPassword parameter is always supplied, deploy.yml:1296, so the active-in-production state rests on the variable, which source cannot prove), publicNetworkAccess: 'Enabled' (infra/main.bicep:861) plus the AllowAzureServices 0.0.0.0 firewall rule (:867), and azureADOnlyAuthentication deliberately not set (:876, the password path retained as a dormant fallback), all documented-accepted. The three anonymous gRPC surfaces are documented-accepted |
| 12 | Performance & Scalability | 2 | 3 | 9 | 6/18 | Uniform async (zero sync-over-async in ADC Source; the single regex hit is a doc-comment cref at Conference.UI/Services/Events/SessionizeRefreshOutcome.cs:8), server-side paging + AsNoTracking + output caching on hot read paths, Polly resilience, and a k6 load test sized to measured conference peak rather than guesswork. Scale substance: Azure Managed Redis is the shared distributed cache, every app scales minReplicas: 1 / maxReplicas: 2 (infra/main.bicep:1916,:2069,:2204,:2390,:2543,:2694; the hub's cross-replica fan-out is proven by Tests/Integration/MMCA.ADC.CrossService.IntegrationTests/CrossService/TwoReplicaHubFanOutTests.cs:49), every service publishes ReadyToRun, and the Conference output cache is Redis-backed with cross-process invalidation: AddRedisOutputCaching (Source/Services/MMCA.ADC.Conference.Service/Program.cs:195), an L1/L2 HybridCache when Redis is present (:203), a deny-by-default base policy (:229) and AddOutputCacheEvictionHandler (:302) consuming OutputCacheEvictionRequested so writes evict tags in about a broker round trip with TTL as the backstop. The client-side half is measured: the WebVitalsTests Playwright tier (Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/WebVitalsTests.cs:29) captures LCP/CLS/FCP/TTFB (plus a single-interaction INP sample) on /, /conference/events and /login and asserts them as hard budgets inside the deploy-gating chromium e2e-gate. The capacity proof measures the deployed topology: load-test.yml run 35013811738 (workflow_dispatch) concluded success with the thresholds http_req_failed rate<0.01 and http_req_duration p95<800 ms (Tests/Load/k6/conference-read-load.js:56-57) holding on the 0.25 vCPU / 0.5 Gi services (infra/main.bicep:1719, repeated at :1968,:2111,:2260,:2450,:2601); the 67-VU default (the observed peak) applies to manual dispatch, and the scheduled run loads 40 VUs (load-test.yml:75). Backpressure is configured rather than left to provider defaults (ArchitectureEvaluationCriteria.md:390): MessageBus:PrefetchCount 16 / ConcurrentMessageLimit 8 in all four service appsettings (Conference.Service/appsettings.json:51-52, Engagement :71-72, Identity :53-54, Notification :85-86), and the pinned framework applies both knobs, only when greater than zero, to every receive endpoint on both broker transports (MMCA.Common.Infrastructure/DependencyInjection.Messaging.cs:356, call sites :274 and :314). Held at 9, not 10: the Conference UI services do not opt into IUiReadCache (zero occurrences in ADC Source; the opt-in caused e2e-gate staleness failures), so the UI read tier is uncached by deliberate choice. Maturity 3, held by decision: the k6 tier executes on a monthly cron plus dispatch only (load-test.yml:23), never inside the deploy chain, and load-freshness gates on the age of the last successful run rather than on execution (deploy.yml:863, window-days: '35' at :872, break-glass skip at :874, in deploy.needs at :1159), so a provisioning change under a still-fresh proof triggers nothing. An in-band proof would load production on every deploy at a ~67 peak, so the monthly run plus load-freshness is the accepted posture (recorded in the backlog's Deliberate and accepted). Evidence: Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/WebVitalsTests.cs:29; Tests/Load/k6/conference-read-load.js:56-57; .github/workflows/load-test.yml:23,:75; deploy.yml:863,:872,:874,:1159; infra/main.bicep:1719,:1916; Source/Services/MMCA.ADC.Conference.Service/Program.cs:195,:201-204,:230,:303; Source/Services/MMCA.ADC.Conference.Service/appsettings.json:51-52; MMCA.Common.Infrastructure/DependencyInjection.Messaging.cs:356 |
| 13 | Observability & Operability | 2 | 4 | 9 | 8/18 | Structured logging + OTel traces/metrics with correlation, liveness/readiness probes driving ACA, workspace App Insights, codified SLO alerts + action group + cost budget in IaC, deliberate telemetry noise/cost control, an Azure Monitor SLO workbook (dashboard) in IaC (infra/main.bicep:772) and a dedicated per-alert Operations Runbook (infra/OPERATIONS.md:13 per-alert triage for failed-requests/response-time/dependency-failures, :55 recovery moves). Maturity 4: ObservabilityConventionTests embeds infra/main.bicep + infra/OPERATIONS.md as manifest resources (MMCA.ADC.Architecture.Tests.csproj:17-22) and fails the build when any sloAlertSpecs entry lacks a severity-correct runbook section or a runbook section is orphaned (ObservabilityConventionTests.cs, a sealed subclass; the four [Fact]s live in MMCA.Common.Testing.Architecture/Bases/Governance/ObservabilityConventionTestsBase.cs), with a minimum-spec floor of 6 keeping the parse non-vacuous (ObservabilityConventionTests.cs:14, over the base default of 3 at :39; corrected 2026-10-19); the test runs in the CI.slnf arch gate (MMCA.ADC.CI.slnf:58, executed by deploy.yml:366, gating deploy via the required PR checks + deploy.yml:1310). Held at 9, not 10 (as scored 2026-10-18): the hold rested on enabled production alerts with no runbook triage section, including the single severity-1 gateway-availability alert. Corrected 2026-10-19, score not re-assessed: that premise no longer holds. All nine alerts outside sloAlertSpecs carry a #### triage section (infra/OPERATIONS.md:174; gateway availability at :463, the security-signal rules added in the 2026-10-19 drift wave at :399 and :441), so the hold is for the next re-score to revisit. Evidence: MMCA.Common.Aspire/Extensions.cs:164,175 (MMCA.Common.Outbox meter + telemetry source), :330,334,350 (/health + /alive + /health/ready split; all consumed via AddServiceDefaults()); infra/main.bicep:255 (workspace App Insights), :357 (action group), :390 (sloAlertSpecs, the gate's parse surface), :483 (the SLO metricAlerts loop), :881 (the sev-1 gateway-availability alert as of 2026-10-19, severity: 1 at :887, outside the gate, triage at infra/OPERATIONS.md:463), :772 (SLO workbook), :793 (monthly cost budget); infra/OPERATIONS.md:1 |
| 14 | Testability & Test Strategy | 3 | 4 | 9 | 12/27 | Healthy non-inverted pyramid: 1507 unit/UI [Fact]/[Theory] across 223 files (including the headless MMCA.ADC.Gateway.Tests tier, SecurityHeaders + GracefulShutdown, in CI.slnf); 54 architecture test classes (56 .cs files), most inheriting their rule bodies from the shared MMCA.Common.Testing.Architecture bases via thin sealed subclasses (including DataResidencyTests/ConstructorDependencyCountTests/BrandColorTokenTests and ObservabilityConventionTests, the §13 alert-runbook pairing gate), plus 14 ADC-local [Fact]/[Theory] methods; 303 gating integration methods across four per-service WAF tiers (Identity 62 / Conference 210 / Engagement 17 / Notification 14), plus 9 non-gating CrossService broker round-trip tests (Testcontainers RabbitMQ+SQL, nightly cross-service-tests.yml); shared E2E infra; deterministic CI gates. The integration tier gates every deploy. A 55.5% unit-tier line-coverage floor on ADC's own code hard-fails the required PR check build-and-test (PR-only, deploy.yml:220; floor at :406 inside the enforcement step at :378-408); the measured filter excludes the service hosts and the protobuf-generated contracts assemblies, which are integration-tier subjects (52.8% raw vs 62.5% filtered on the same run, deploy.yml:387-392, filter at :404). The chromium E2E suite gates deploy via the e2e-gate job. Held at 9, not 10: the genuine broker round-trip is non-gating nightly and the SQL integration suite runs only in CI (not headless-verifiable). Evidence: Tests/Architecture/MMCA.ADC.Architecture.Tests/; Tests/Integration/MMCA.ADC.{Identity,Conference,Engagement,Notification}.IntegrationTests + MMCA.ADC.CrossService.IntegrationTests; deploy.yml:1159 (deploy needs [changes, supply-chain, cost-guard, freshness, e2e-gate, ai-eval-gate, foundation, build-images]; build-and-test/integration-tests/coverage gate as required PR checks), :406 (coverage floor 55.5%, gating); e2e.yml (workflow_call + alternating nightly legs); cross-service-tests.yml (weekday-nightly, advisory) |
| 15 | Best Practices & Code Quality | 2 | 4 | 8 | 8/16 | Enforcement is genuinely strong: five analyzers at error severity repo-wide (Directory.Build.props:117), warnings-as-errors (:10), AnalysisLevel/AnalysisMode=All (:11-12), CodeAnalysisTreatWarningsAsErrors (:13), EnforceCodeStyleInBuild (:14), blanket dotnet_analyzer_diagnostic.severity = error (.editorconfig:312), central package management with committed lock files (Directory.Build.props:36), consistent Result-pattern error handling, rare hand-written pragma disables in Source/, most with an inline reason (LiveEventListener.razor.cs:95) and one bare (S1075 at ADCHome.razor.cs:33), and a Justification on all nine in-source SuppressMessage attributes (e.g. Identity.Domain/Users/User.cs:119), all enforced by the required build-and-test Release build with a --locked-mode restore (deploy.yml:297) and the gating suppress-aware vulnerable-package audit (:608-610), both required per CONTRIBUTING.md:80. Suppression hygiene: Directory.Build.props (263 lines) carries five NuGetAuditSuppress items (:55-59), the ImageSharp 3.1.12 advisory hold, argued in place (:46-53: transitive through MMCA.Common.Infrastructure, patched only in 4.1.2, which the v3 license pin rules out, with Common's ImageSharpImageProcessor the single decode path) and mirrored from MMCA.Common; the SSH.NET high advisory is remediated by a patched pin rather than suppressed (Directory.Packages.props:83, SSH.NET 2026.0.0). NoWarn hygiene: the global NoWarn line is three codes, CS1591;EXTEXP0001;S8970 (:31), each with a justification immediately above it (:16-21, :22-26, :27-30; the S8970 block carries no review date), and RMG020 sits in an .Application-scoped PropertyGroup (:79, rationale :72-78) rather than on the global line. The test-tier sets are central too: suffix-conditioned PropertyGroups with per-code reasons (:65-104), so no test csproj declares <NoWarn>; the only csproj-level NoWarn lines are ASPIRE010 in the AppHost (MMCA.ADC.AppHost.csproj:11) and CA5392 in the MAUI head. Held at 8 (TD-18): the MAUI MMCA.ADC.UI project is outside every CI build (MMCA.ADC.CI.slnf lists only UI.Web :25 and UI.Web.Client :26, and the weekly maui-audit.yml installs the maui-android workload, :109, only to restore and audit, :93, :128, never to build), so its analyzers/TWAE are review-only and its own NoWarn CA5392 is never gated (MMCA.ADC.UI.csproj:155), while the gating vulnerable-package scan (deploy.yml:610) is CI.slnf-scoped. .github/workflows/maui-audit.yml covers the supply-chain half only: a Sunday-cron plus dispatch (:36), android-only (:53), non-gating detector in no deploy.needs, whose own header claims only the supply-chain half (:14). Evidence: Directory.Build.props:10,:11-12,:13,:14,:16-21,:22-26,:27-30,:31,:36,:46-53,:55-59,:65-104,:72-78,:79,:117; .editorconfig:312; Directory.Packages.props:83 (patched SSH.NET pin), :98-130 (the MMCA.Common.* pin range); MMCA.ADC.CI.slnf:25,26; deploy.yml:297 (--locked-mode restore on the gating build), :608-610 (the audit step), :610 (the CI.slnf-scoped vulnerable-package scan); maui-audit.yml:14,36,53; CONTRIBUTING.md:80 |
| 16 | AI-Native Application Architecture | 2 | 4 | 10 | 8/20 | Scored because organizer-facing AI session scoring calls a model in production (commit 5c082663; applicability rule ArchitectureEvaluationCriteria.md:473). Every criterion the feature exercises is met at the pipeline boundary, with no red flag and no non-trivial polish left. (1) Model calls behind a port (:476): IAiScoringService is an Application interface with no SDK type (Source/Modules/Conference/MMCA.ADC.Conference.Application/Sessions/UseCases/DecisionSupport/ScoreEventSessions/IAiScoringService.cs:6), the adapter, prompt and PromptContract are Infrastructure over the framework's governed IChatClient (Source/Modules/Conference/MMCA.ADC.Conference.Infrastructure/Sessions/Scoring/SessionScoringService.cs:56, :93), and AiDependencyIsolationTests (Tests/Architecture/MMCA.ADC.Architecture.Tests/Layering/AiDependencyIsolationTests.cs:14, in the PR-gating MMCA.ADC.CI.slnf:59) fails the build if the SDK or MMCA.Common.AI reaches a layer outside Infrastructure. (2) Prompt and model versioning (:477): a dated PromptVersion (SessionScoringService.cs:84) SHA-256-pinned against a recorded hash, so a prompt edit without a bump fails and a bump without a recorded hash fails (PromptContractTests.cs:57, :63), and the model id pinned to ONE source, ModelIdValue (SessionScoringService.cs:63) asserted equal to the deployed Ai:Model (Conference.Service/appsettings.json:95). (3) Evaluation gates CI (:478): seven golden cases (Golden/case-*.json) replay through the real service on every PR (MMCA.ADC.CI.slnf:49) plus the deploy-gating ai-eval-gate job (deploy.yml:510), and both evaluation tiers exercise the pipeline the host deploys: BuildGovernedProvider calls AddConferenceAiGuardrails(configuration) (GoldenReplayTests.cs:183, :196), every case asserts GuardrailChatClient is in the resolved chain (:53, :60), and the paid judge composes the same call (LiveJudgeTests.cs:111). (4) Guardrails at the boundary (:479), met on all four halves at the boundary rather than inside the feature: the composition is one list, AddConferenceAiGuardrails(configuration) in Conference Infrastructure DI (Source/Modules/Conference/MMCA.ADC.Conference.Infrastructure/DependencyInjection.cs:85, registering AddPiiRedactionGuardrail() at :95, AddContentPolicyGuardrail(configuration) at :96 and AddSessionScoreResponseGuardrail() at :99), called by the host before AddMmcaChatClient reads the descriptors (Conference.Service/Program.cs:150, :152); output validation is SessionScoreResponseGuardrail.InspectResponseAsync (Source/Modules/Conference/MMCA.ADC.Conference.Infrastructure/Sessions/Scoring/SessionScoreResponseGuardrail.cs:74), refusing a provider refusal, an empty answer, unparseable text and a schema-shaped object missing a sub-score (:40, :44, :51), scoped to the scoring call by the prompt name read off the contract (:63, against SessionScoringService.PromptName at SessionScoringService.cs:56) so a second AI feature on the same governed client is untouched by it; the input half is the delimited <session_proposal> envelope with angle-bracket escaping and an explicit untrusted-input brief (SessionScoringService.cs:266, :271, :281); redaction is a pipeline policy the feature cannot skip (MMCA.Common.AI/Guardrails/PiiRedactionGuardrail.cs:39 implements both IChatRequestRedactor and IChatGuardrail, placeholders at :41) and the contact-details golden case asserts the redaction reached the provider rather than merely being registered (GoldenReplayTests.cs:73, :143, :155, :156, keyed on Golden/case-contact-details-in-bio.json at :160); and the content policy the feature cannot bypass is the framework ContentPolicyGuardrail in Redact mode, configured from Ai:ContentPolicy in the deployed settings (Conference.Service/appsettings.json:104, :105; the type implements both halves at MMCA.Common.AI/Guardrails/ContentPolicyGuardrail.cs:64, :115), running inside GuardrailChatClient with no per-call opt-out (MMCA.Common.AI/Chat/GuardrailChatClient.cs:70, :77) under Ai:RequireGuardrail, default true, which makes an enabled host with nothing registered a startup failure (MMCA.Common.AI/AiSettings.cs:110, MMCA.Common.AI/DependencyInjection.cs:208). The wiring is pinned: ConferenceAiGuardrailsRegistrationTests asserts exactly three IChatGuardrail and two IChatRequestRedactor registrations by type (Tests/Modules/Conference/MMCA.ADC.Conference.Infrastructure.Tests/Services/ConferenceAiGuardrailsRegistrationTests.cs:81, :90, :98), that calling it twice duplicates nothing (:106, :117, :118), and that an enabled pipeline composes GuardrailChatClient end to end (:123, :139), in a project inside MMCA.ADC.CI.slnf:48, so dropping ONE registration reds a PR. (5) Least-privilege tool calling (:480): AllowTools:false strips tools at the framework boundary (Conference.Service/appsettings.json:98) and the one consequential action, a paid full-event pass, is human-triggered and permission-gated on SessionSelectionManage (SessionSelectionController.cs:32). (6) Retrieval (:481): unexercised and not a deduction: ADC has no vector or hybrid store, the criterion is worded conditionally on one existing, and no red flag names retrieval. (7) LLM observability and cost (:482): mmca.ai.input_tokens, mmca.ai.output_tokens and the mmca.ai.call.duration histogram are tagged by model, prompt name and prompt version (MMCA.Common.AI/Observability/AiUsageMeter.cs:29, :32, :45, :163), mmca.prompt.name/.version/.hash are set as span tags on the governed activity by PromptTaggingChatClient (MMCA.Common.AI/Chat/PromptTaggingChatClient.cs:10, :67), the Conference host subscribes both signals under the one name (Conference.Service/Program.cs:171, :172), and the deployed spend ceiling is the sixth entry inside sloAlertSpecs (infra/main.bicep:407, key at :494, severity 3 at :500, consumed by the alert loop at :508; re-anchored 2026-10-19), so the alert-runbook pairing gate parses it against its runbook section (infra/OPERATIONS.md:136) with MinimumAlertSpecs => 6 (Tests/Architecture/MMCA.ADC.Architecture.Tests/Governance/ObservabilityConventionTests.cs:14). Maturity 4: the boundary, the prompt hash, the model-id pin, the golden replay, the guardrail-composition test and the alert-runbook pairing are PR-gating fitness tests, ai-eval-gate is a hard deploy precondition (deploy.yml:1185), the spend ceiling is IaC, and the feature is governed by ADR-111 and ADR-120. Why 10: no rubric red flag is present (ArchitectureEvaluationCriteria.md:485-:489), and what remains is polish of the kind ArchitectureEvaluationCriteria.md:60 permits at 10. Four trivial residuals, none a deduction: (a) the paid live-judge tier is post-merge only, since ai-eval-gate is skipped on pull requests (deploy.yml:512, the judge step at :544) while the replay and contract tiers are PR-covered (MMCA.ADC.CI.slnf:48-:49); (b) Ai:PerCallInputTokenBudget is unset for ADC (the Ai block at Conference.Service/appsettings.json:93-:106 carries no budget entry), so the framework's input-size bound never evaluates (MMCA.Common.AI/Chat/BoundedChatClient.cs:279, setting at MMCA.Common.AI/AiSettings.cs:124) and input size is bounded only by entity max lengths; (c) Ai:ContentPolicy configures only InjectionMode (appsettings.json:104-:105), leaving BlockedResponsePatterns empty (MMCA.Common.AI/Guardrails/ContentPolicySettings.cs:57), so the framework policy's answer half is inert here and the answer side is carried by the scoring guardrail alone; (d) the complete-score-object rule is declared three times (the structured-output required list at SessionScoringService.cs:383, AiScoreResponse.IsComplete at AiScoreResponse.cs:63, and BuildResult at SessionScoringService.cs:173), so a seventh sub-score means three edits. Each is one configuration entry or one de-duplication. The Maintainability & Evolvability criteria of rubric v1 are scored under §34, §32 and §33 |
| 17 | DevOps & Deployment | 2 | 4 | 9 | 8/18 | CI gates build/analyzers/unit+arch/integration/model-drift/coverage-floor on every PR, and the supply-chain vuln-audit + SBOM are blocking PR gates; CD fully automated via multi-phase Bicep IaC with OIDC + managed identity (no standing creds); post-deploy smoke gate with revision-copy auto-rollback plus a scheduled cost-drift guard. App→DB auth is passwordless managed identity in production (useManagedIdentitySql=true), so the CD path ships no runtime SQL admin login/password to the apps. Held below 10 only by the deferred-by-design SQL public network access (main.bicep:861, accepted, no VNet). Evidence: deploy.yml:3-6 (push+PR), :450-465 (model-drift gate), :394 (coverage floor), blocking supply-chain audit+SBOM+licence gates (audit exit :425, SBOM exits :451/:454), :1159 (deploy needs changes+supply-chain+cost-guard+dr-freshness+load-freshness+cross-service-freshness+e2e-gate+foundation+build-images), :1470-1641 (Phase-5 smoke + auto-rollback, with an explicit alarm when a rollback itself fails), :234 (expand/contract migration guard, since revision rollback does not revert schema), OIDC no standing creds; passwordless MI DB auth active (main.bicep:857-858 SQL admin login as dormant fallback, :876 records that azureADOnlyAuthentication is deliberately unset); main.bicep:861 public network access + :867 the AllowAzureServices firewall rule (accepted) |
| 18 | UI Architecture & Components | 3 | 4 | 8 | 12/24 | Maturity 4: UIArchitectureConventionTests.cs:12 (a sealed subclass of the shared UIArchitectureConventionTestsBase) machine-enforces the container/presentational split (code-behind presence and inline @code caps on routable pages), the arch-test project is in MMCA.ADC.CI.slnf:58, and that filter is built and tested by the build-and-test job (deploy.yml:303,366), a required check on every PR (CONTRIBUTING.md:80), meeting the rubric's automatically-enforced bar (ArchitectureEvaluationCriteria.md:29). The split is genuinely disciplined (routable @page components carry .razor + .razor.cs code-behinds; no business logic in markup, data via injected UI services), with typed parameter/EventCallback contracts using EditorRequired, composition via RenderFragment, and heavy reuse of a shared Common.UI base class plus primitives (none of the blob/prop-drilling red flags). Code-behind size against the enforced 400-line cap (unoverridden, UIArchitectureConventionTests.cs:10-12): the high-water mark is ADCHome.razor.cs at 370 (30 lines of headroom), then EventDetail 354, EventFeedback 351, SessionAssetsPanel 349, SessionLive 347, SpeakerDashboard 340, SpeakerDetail 339, PublicSessionDetail 334, so eight files are within 70 lines of the cap; PublicSessionList.razor.cs (328) and SessionFeedback.razor.cs (299) are extracted, into PublicSessionEventCatalog and SessionFeedbackAnswers. Held at 8: presentational sub-component extraction is applied file by file rather than systematically (TD-16, resolution path per the TD-13 pattern). The cap stays at 400 by decision: lowering it to 300 would force about twelve extractions at once, so only the files near 400 are extracted (backlog Deliberate and accepted). Evidence: Tests/Architecture/MMCA.ADC.Architecture.Tests/UIArchitectureConventionTests.cs:12 (the §18 gate); MMCA.ADC.CI.slnf:58 + deploy.yml:366 (gates every PR); Common UIArchitectureConventionTestsBase.cs:22 (MaxCodeBehindLines => 400, MaxInlineCodeLines => 120; no ADC override, UIArchitectureConventionTests.cs:10); Conference.UI/Pages/Home/ADCHome.razor.cs (370, the high-water mark) and Conference.UI/Pages/Events/EventDetail.razor.cs (354); Common DataGridListPageBase.cs:18 (shared abstract base); Conference.UI/Pages/Event/EventList.razor.cs:16,21 (@inherits base, [Inject] IEventUIService, no data access) |
| 19 | State Management & Data Flow | 3 | 4 | 9 | 12/27 | Maturity 4: StateManagementConventionTests.cs:11 (a sealed subclass of the shared StateManagementConventionTestsBase) machine-enforces the two §19 rules: UI assemblies carry no mutable static state (base :27; a static member is shared across every Blazor Server circuit) and stateful *StateService/*StateContainer services stay scoped, never singleton (base :65); the ADC subclass declares no exemptions, and the arch-test project runs in the merge/deploy gate (MMCA.ADC.CI.slnf:58), so the per-circuit ownership model is Optimized (4), the same rationale as §18. Implementation 9: single-source-of-truth state with explicit ownership (component-local vs per-circuit scoped service), zero mutable static user/session fields (machine-proven), unidirectional flow with immutable records, StateHasChanged/InvokeAsync used intentionally, the stale-IsDirty red flag pre-empted via IsDirtyAccessor; PersistentComponentState avoids per-render refetch; the disposed-CTS race is tolerated. Evidence: Tests/Architecture/MMCA.ADC.Architecture.Tests/StateManagementConventionTests.cs:11 (the §19 gate); Common StateManagementConventionTestsBase.cs:27,65 (the two rules); MMCA.ADC.CI.slnf:58; Common ListPageStateService.cs:9,81 (per-circuit scoped, immutable record with); Common DependencyInjection.cs:55 (TryAddScoped); UnsavedChangesGuard.razor:32; DataGridListPageBase.cs:538-560 |
| 20 | Design System & UI Consistency | 2 | 4 | 9 | 8/18 | Implementation 9 is held by decision over a proposed 9→8. MudBlazor used consistently with a single centralized theme: BrandColors is the one C# source of truth for the palette (MMCA.Common.UI/Theme/BrandColors.cs:26, Secondary #00796B for WCAG 5.3:1), MMCATheme supplies both palettes, the typography scale and border radius (Theme/MMCATheme.cs:56 PaletteDark), and Day/Dark is a real capability wired through @bind-IsDarkMode inside the consolidated MmcaThemeProviders component (Theme/MmcaThemeProviders.razor:12; Layout/MainLayout.razor:14,:35 for the providers and the ThemeToggle), which ADC reuses in its one custom layout (Engagement.UI/Layouts/PresenterLayout.razor:6). The ADC landing page is brand-token-clean: ONE shared ADCHome.razor.css rendered by both hosts uses var(--mmca-primary) (:249,:260,:285,:385,:425,:450), guarded by Tests/Architecture/MMCA.ADC.Architecture.Tests/Ui/BrandColorTokenTests.cs:12 (a sealed subclass of the shared base, which asserts the stylesheet contains var(--mmca-primary) and not the literal #1565C0, BrandColorTokenTestsBase.cs:25; the sheet is embedded as ADCHome.Shared.razor.css, MMCA.ADC.Architecture.Tests.csproj:11, in the CI filter MMCA.ADC.CI.slnf:59). Shared PageHeader / EmptyState / PageLoadingState / PageErrorState primitives are defined once in the framework stylesheet (MMCA.Common.UI/wwwroot/app.css:261) and used 87 times across 43 ADC razor files; the MudDataGrid v9 RowsPerPage/CurrentPage guardrail is wrapped once (DataGridListPageBase.cs:294,:407, BL0005 justified in place) and subclassed by roughly ten ADC list pages. The Secondary tokens are guarded: --mmca-secondary and --mmca-secondary-dark are covered and ThemeSecondary_IsSourcedFromBrandColors asserts the whole Secondary family in both palettes (MMCA.Common.UI.Tests/Theme/BrandColorTokenTests.cs:38,:60). Inline styles are zero and pinned: layout and sizing ride semantic classes in Conference.UI/wwwroot/adc-ui.css (for example .text-center-cell at :75, .stat-col at :84), and Tests/Architecture/MMCA.ADC.Architecture.Tests/Ui/InlineStyleTests.cs:22 fails the CI architecture tier on any style=, Style=, CellStyle=/HeaderStyle= or *StyleFunc= in a .razor file under Source/ (no allow-list, :13; a floor of 80 scanned files, :19, keeps the scan from passing vacuously). Watch item, recorded and not deducted: the untokenized landing gradient and accent hexes documented with a contrast rationale (ADCHome.razor.css:16,:77,:549), and eight !important overrides in Common's shared app.css (:189,:209-210,:297,:302,:307-308,:314, each with a mobile/a11y rationale; a generalized selector at :206). Evidence: Common BrandColors.cs:26, MMCATheme.cs:56, MmcaThemeProviders.razor:12, MainLayout.razor:14,:35, app.css:206,:261,:297, DataGridListPageBase.cs:294,:407, MMCA.Common.UI.Tests/Theme/BrandColorTokenTests.cs:38,:60; ADC ADCHome.razor.css:16,:249, Ui/BrandColorTokenTests.cs:12, MMCA.ADC.Architecture.Tests.csproj:11, MMCA.ADC.CI.slnf:59, PresenterLayout.razor:6, adc-ui.css:75,:84, Ui/InlineStyleTests.cs:13,:19,:22 |
| 21 | Accessibility (a11y) | 3 | 3 | 8 | 9/24 | Implementation is strong: a stated WCAG 2.1 AA target, real semantic landmarks/skip-nav/focus/aria-labels, native controls, tuned contrast, axe-core in shared E2E bases ADC consumes, and 45 axe scans (AccessibilityTests.cs:23, including the Partner pages and the conference-day surfaces), with a titled, sandboxed venue-map iframe, plain labels in place of dead chip toggles, and Happening Now title links. Keyboard operability has one E2E gate, on the live-moderation dialog (Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/Engagement/LiveModerationKeyboardTests.cs:23, a subclass of Common's KeyboardOperabilityTestsBase). The chromium E2E/axe suite blocks production deploys (e2e-gate, deploy.yml:806), though the gate is non-PR and conditional (the deploy job accepts skipped at :1196). Maturity 3: the rubric's §21 verification criterion pairs automated CI checks with periodic manual screen-reader/keyboard passes, and the results table in adc-ACCESSIBILITY-SCREENREADER-PASS.md (Website docs-src/guides/) holds only the empty placeholder row (:64): the runbook is ready and the dated pass, the maturity-4 lever, is pending. Evidence: Common MainLayout.razor:18,21,29,67,79 (skip-nav, role landmarks, role=alert); EventList.razor:12,55,125 (aria-label icon buttons; 100 aria attributes across 42 ADC .razor files); Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/AccessibilityTests.cs:23; Workflows/Engagement/LiveModerationKeyboardTests.cs:23; deploy.yml:806,:1196; adc-ACCESSIBILITY-SCREENREADER-PASS.md:64 |
| 22 | Responsive & Cross-Browser | 2 | 3 | 7 | 6/14 | Maturity 3: one of the category's criteria is enforced automatically. A cross-browser-freshness step of the freshness job (deploy.yml:923, documented in ADC AGENTS.md:79) runs the shared fail-closed Common freshness-gate action (MMCA.Common/.github/actions/freshness-gate/action.yml:96, break-glass skip with a required justification) over a ten-day window (deploy.yml:936), requiring a successful E2E (firefox) and a successful E2E (webkit) job; freshness is in deploy.needs (:1159) and is a hard condition of the deploy job (needs.freshness.result == 'success', :1193). That gate enforces recency of a weekly per-engine leg (the nightly alternates firefox and webkit with continue-on-error on the schedule, e2e.yml:147), and the deploy e2e-gate is chromium-only (deploy.yml:806, browsers: '["chromium"]' at :820). Responsive layout itself has no automated guard: no ADC E2E test sets a phone viewport, no responsive fitness test exists in Tests/Architecture (the Ui/InlineStyleTests.cs:7 guard covers §20 inline styles), and bUnit asserts the mobile branch on some list pages only (Identity.UI.Tests/Pages/Users/UserListTests.cs:331). The rubric's Optimized bar means enforced automatically (ArchitectureEvaluationCriteria.md:39); §29, §31 and §33 hold M4 on freshness gates because there the gated proof is the category's substance, while here the gate covers the browser matrix and leaves reflow, density and touch targets to review (M3, consistent and review-enforced). Implementation 7, two deductions: (1) density options (ArchitectureEvaluationCriteria.md:626): the framework ships the capability (DataGridListPageBase.cs:78 DenseGrid, :909 ToggleDensity(), documented in common-RESPONSIVE.md:47-50) and ADC adopts none of it, zero matches for DenseGrid, ToggleDensity or a Dense="@..." binding across MMCA.ADC/Source, with list pages hardcoding a fixed density (EventList.razor:73 Dense="true"); (2) content reflow (:625): 20 .razor files render raw MudSimpleTable/<table> outside the DataGrid's grid-to-card degradation; 12 are key/value detail tables that wrap on mobile through .detail-table (Conference.UI/wwwroot/adc-ui.css:21-40, :33), while the session-selection statistics tables use fixed-width columns with no mobile alternative (adc-ui.css:80) and fall back to the Common horizontal scroller. The positives holding the 7: BreakpointConstants.cs:16 + DataGridListPageBase.cs:308 drive grid-to-card below 960px on every list page, Common's app.css:204 holds mobile icon buttons and the pager at 48px, and the Chromium/Firefox/WebKit matrix is gated on recency. common-RESPONSIVE.md:65 describes Common's own required Firefox/WebKit merge gate, which ADC does not have. The freshness gate changes enforcement, not the responsive substance, so it does not count toward implementation |
| 23 | Front-End Performance | 2 | 4 | 8 | 8/16 | Server-side paging/filtering/sorting (no load-everything-then-page-in-memory), bounded virtualized infinite scroll with @key, debounced search, request cancellation, SSR prerender + PersistentComponentState handoff, and output caching: strong client-side hygiene. Maturity 4: the Core Web Vitals budgets are real failing assertions (WebVitalsTests.cs:87 asserts the shared CWV budget declared at :35 on four measured pages, facts at :44, :55, :69, :90) and the suite runs unfiltered inside the deploy-gating chromium e2e-gate (deploy.yml:806 e2e-gate job, in deploy.needs at :1159), so a red vitals assertion blocks the production deploy: the same deploy-gate = M4 standard applied to §28/§29/§31. It is a deploy gate, not a PR merge gate, which meets the rubric's automatically-enforced bar (ArchitectureEvaluationCriteria.md:29). A Brotli WASM payload budget runs on every code PR (wasm-payload-budget, deploy.yml:476, budget-kb: 5900 at :497) but is not a required check (:472). Implementation 8: images are not optimized and the WASM bundle loads full ICU data rather than the EFIGS shard (BlazorWebAssemblyLoadAllGlobalizationData at MMCA.ADC.UI.Web.Client.csproj:11); code-splitting the bundle is declined by decision (rejected in Common, common-RemediationBacklog.md #23). Evidence: Common EntityServiceBase.cs:51 (server-side paging/sort/filter via query params); DataGridListPageBase.cs (CTS-cancelled ServerData + prerender-bounded fetch + persist/restore); MobileInfiniteScrollList.razor:16 (@key) |
| 24 | Forms, Validation & UX Safety | 2 | 4 | 8 | 8/16 | Client validation mirrors most server FluentValidation rules, the unsaved-changes guard reads current dirty state via a live accessor (avoids the stale-IsDirty red flag), submit is disabled while saving (no double-submit), _isDirty clears before NavigateTo on success (no false prompt), and destructive actions are confirmed via MudMessageBox. All 21 MudForm files render an <ErrorSummary> with a localized heading (EventCreate.razor:41), and the exception snackbars are localized whole-sentence keys (Profile.razor.cs:235 Snackbar.ChangePasswordFailed, no {ex.Message} interpolation). Maturity 4: Ui/FormsConventionTests.cs (a subclass of the shared FormsConventionTestsBase) enforces the guard, dirty-tracking and error-summary markers across the create forms (MinimumCreateForms => 9 at :17) and the Profile form, all in the CI.slnf arch gate (MMCA.ADC.CI.slnf:58). A bUnit render-level assertion of the summary's error items is CI-gated. The format half of client/server parity is shared: SessionizeCodeAttribute.cs:41 calls the same SessionizeCodeFormat.IsValid the server rule calls, and EventFormModel.cs:44 sources format and length rules from the DTO constants. The cross-field half is mirrored too: the Session forms check BR-122 (EndsAt strictly after StartsAt, server rule Session.cs:207) through SessionFormModel.HasEndAfterStart (Pages/Sessions/SessionFormModel.cs:87-92) on both the create (SessionCreate.razor.cs:162-166) and edit (SessionDetail.razor.cs:186-191) paths, as the Event (EventCreate.razor.cs:75) and Activity (ActivityCreate.razor.cs:167) forms do for their range rules, and a server rejection surfaces the Result error (SessionCreate.razor.cs:178, SessionDetail.razor.cs:196-197). Accessible field errors are asserted at render level on one form: ShouldExposeFieldErrorAccessibly checks role=alert, aria-invalid and aria-describedby on EventCreate (Tests/Modules/Conference/MMCA.ADC.Conference.UI.Tests/Pages/Events/EventCreateTests.cs:104). Held at 8: the forms fitness gate is string-marker based and scans only the create forms and Profile, so detail/edit and feedback forms are not fitness-pinned, and the accessible-error assertion covers one of the 21 MudForm files. Lever to 9: extend ShouldExposeFieldErrorAccessibly to the remaining MudForm pages. Evidence: Tests/Architecture/MMCA.ADC.Architecture.Tests/Ui/FormsConventionTests.cs:17; EventCreateTests.cs:104; Common FormsConventionTestsBase.cs:29; MMCA.ADC.CI.slnf:58; EventCreate.razor:8,41 (guard + per-form summary); EventCreate.razor.cs:57,89,125 (client validation before submit; _isDirty=false before NavigateTo; Disabled while IsSaving); Profile.razor.cs:235,251 (localized snackbar keys; MudMessageBox delete confirm); Conference.Application/Events/UseCases/Create/EventCreateRequestValidator.cs:9 (server-side parity) |
| 25 | Navigation & Information Arch | 2 | 4 | 8 | 8/16 | The substance is Strong: clean bookmarkable routes via RoutePaths constants, route-level guards on every routable page (server-enforced via SSR cookie auth, not just UI hiding), expressed as the RoleNames.Organizer constant rather than a magic string (EventList.razor:2), role/claim/permission-filtered menu rendering (Common NavMenu.razor:257 on RequiredRole / RequiredClaim / RequiredPermission), 404 (NotFoundPage, Routes.razor:14) and 403 (dedicated <Forbidden/>, Routes.razor:49) handled in the shell, and breadcrumbs throughout. adc-NavigationFlow.md (Website docs-src/guides/) carries per-actor flows, the Activity routes (5.9 at :596), role administration (5.10 at :614), the page-guard enumeration (:715-716) and the scanned-QR deep links (:719), and its Route Contract table (:639) lists all 59 ADC routes with their access. Maturity 4: the doc-to-code match is enforced automatically: Tests/Architecture/MMCA.ADC.Architecture.Tests/Api/NavigationContractTests.cs parses every @page under Source/, requires one recognized guard per page (:44-60), and checks set equality with the Route Contract table (:63-76) and each row's access against the page guard (:79-104), failing rather than skipping when the doc is missing (:246); build-and-test downloads the published doc first (deploy.yml:338-348) and runs the architecture project (MMCA.ADC.CI.slnf:59, deploy.yml:366). The route-authorization fitness tests stay in place (ManagementRouteAuthorizationTests.cs:19, SpeakerDetail pinned Organizer-only at :43; IdentityRouteAuthorizationTests.cs:16). Every parameterized route is typed (:int, or :guid for speakers) with a typed parameter (EventDetail.razor:1, EventDetail.razor.cs:27, SpeakerDetail.razor.cs:34), so a malformed id answers with the not-found page; the speaker self-edit form carries UnsavedChangesGuard (SpeakerDashboard.razor:11); list paging, sort and named filters are mirrored into the query string on each change and read back first on load (Common DataGridListPageBase.cs:978-981,:217-228); and /profile/sessions (reachable by URL, menu link Organizer-only through Layout:SessionsNavRequiredRole, MMCA.ADC.UI.Web/appsettings.json:36) is recorded as deliberate account self-service (adc-NavigationFlow.md:726). Held at 8, not 9: two criteria stay review-enforced. No test ties a nav item's RequiredRole / RequiredClaim to the guard of the page it opens (EngagementUIModule.cs:25-28; NavigationContractTests.cs:44-104 covers routes and page guards only), and the per-actor flow diagrams and the framework-owned routes (/, /profile/sessions, /confirm-email) sit outside the machine-checked Route Contract (adc-NavigationFlow.md:637,:723). The contract gate runs on ADC code diffs only (deploy.yml:339), so a Website-side edit surfaces at the next ADC code PR. Lever to 9: a fitness test pairing each IUIModule.NavItems requirement with its target page's guard. Evidence: Common Routes.razor:14,:49, NavMenu.razor:82-86,:257; EventList.razor:2; Api/NavigationContractTests.cs:44-60,:63-76,:79-104,:246; deploy.yml:338-348; MMCA.ADC.CI.slnf:59; ManagementRouteAuthorizationTests.cs:19,:43; IdentityRouteAuthorizationTests.cs:16; MMCA.ADC.UI.Web/appsettings.json:36; EventDetail.razor:1; EventDetail.razor.cs:27; SpeakerDetail.razor.cs:34; SpeakerDashboard.razor:11; EngagementUIModule.cs:25-28; Common DataGridListPageBase.cs:217-228,:978-981; Website docs-src/guides/adc-NavigationFlow.md:596,:614,:637,:639,:715-716,:719,:723,:726 |
| 26 | Front-End Security | 3 | 4 | 9 | 12/27 | Reference-quality client security: no unsanitized HTML/MarkupString, tokens in HttpOnly cookies with the refresh token unreadable by JS and no localStorage, an enforced CSP with locked script-src/connect-src plus security headers/antiforgery, no secrets in the WASM bundle (runtime config fetch), authorization re-enforced server-side. Held at 9: img-src https: and style-src 'unsafe-inline' are deliberately permissive (documented, low-risk) and emitted by the pinned provider (MMCA.Common.UI.Web/Security/BlazorCspPolicyProvider.cs:106-107); the framework's {nonce} path off unsafe-inline (MMCA.Common.Aspire/Security/SecurityHeaders.cs:62) has zero CspNonce uses in ADC, and appsettings.json:51 adds a third-party frame-src allow-list for three Google origins. Evidence: Hosts/UI/MMCA.ADC.UI.Web/Program.cs:103 (AddSessionCookieAuthentication()), :177 (AddCommonBlazorCsp(), the CSP provider: the CSP, token-storage, and security-headers mechanism lives in MMCA.Common and is consumed here; HttpOnly refresh cookie never JS-readable, no localStorage, locked script/connect-src), :198 (UseCommonSecurityHeaders() enforced CSP), :222 (UseAntiforgery), :227 (UseAuthenticatedNoStore), :247 (MapClientConfigEndpoint, the framework /client-config runtime-config endpoint, no secrets in the WASM bundle) |
| 27 | Internationalization (i18n) | 1 | 4 | 8 | 4/8 | ADC ships real en-US + es localization: 68 base .resx + 68 .es.resx (every base has an .es sibling; covers the module UIs, both ADCHome hosts, the nav items, and the API error-resource sets), IStringLocalizer<T> across every page, zero hard-coded snackbars (whole-sentence page keys; raw {ex.Message} never surfaces), zero literal page titles (the Title => L["Title"].Value pattern on all ~24 pages), zero literal breadcrumbs (built in OnInitialized from Breadcrumb.* keys), no ErrorMessages.Success concatenation (obsoleted in Common), nav menus localized via NavItem.TitleResource, and MudBlazor built-in chrome localized via the framework's ResxMudLocalizer (inherited). Culture alignment and persistence: UI.Web/Program.cs:124 UseCommonRequestLocalization(), UI.Web.Client/Program.cs:86 MmcaCultureBootstrap.SetBrowserCultureAsync, User.PreferredCulture. Maturity 4, doubly gated in CI.slnf: TranslationCompletenessTests.cs:12 (RequiredCultures=[es], floor 40) + LocalizedTextConventionTests.cs (no hard-coded snackbar/title/<PageTitle>/breadcrumb/NavItem literal can ship; MinimumScannedFiles=60). Held at 8: the PseudoLocalizationTests tier covers text expansion on only 3 public pages (/, /conference/events, /conference/sessions, PseudoLocalizationTests.cs:41-43) of the 53 routable @page files, public by design (:25, "no login keeps the gate robust"), so the authenticated authoring surface is unverified for text expansion, and a citable violation of the rubric's culture-aware-formatting criterion also stands. The 8→9 lever (broadening across the authenticated surfaces) is deferred by decision: it is worth 1 weighted point and costs authenticated-login plumbing plus expansion assertions across roughly 45 pages, the weakest cost-to-benefit ratio on either band; its re-open triggers (a second locale beyond es, any RTL locale, or a reported layout regression on an authenticated page) are in the backlog's Deliberate and accepted section. Evidence: Tests/Architecture/MMCA.ADC.Architecture.Tests/{TranslationCompletenessTests.cs:12,LocalizedTextConventionTests.cs}; EventCreate.razor.cs (Title/breadcrumb/snackbar keys), Profile.razor.cs (sanitized snackbars), ConferenceUIModule.cs (TitleResource + resx pair); Common ErrorMessages.cs ([Obsolete] Success), ResxMudLocalizer.cs |
| 28 | Front-End Testing & Quality | 3 | 4 | 9 | 12/27 | bUnit component + route-auth tests gate every PR (MMCA.ADC.CI.slnf:43,:49,:55 via deploy.yml:366, --minimum-expected-tests 1 so a discovery breakage cannot pass vacuously), a broad Playwright suite with shared page objects and abstract bases (46 page objects, 28 workflow suites, no per-app duplication) covers role journeys, 260 role/label/test-id selectors against only 5 sleep-style calls avoid timing flakiness, no skipped or quarantined tests exist under Tests/, and axe runs in the shared Login/Register/Profile bases ADC consumes. What makes it a 9: (1) a markup-snapshot regression tier in all three bUnit projects (Conference.UI.Tests/Components/ComponentsSnapshotTests.cs:27, assertions at :68,:86,:123,:142, plus sibling files under Identity.UI.Tests and Engagement.UI.Tests: 12 [Fact]s over 12 committed .html baselines, deterministic and OS-independent, with a missing baseline reported as a non-match so it cannot pass vacuously), riding the required per-PR check; (2) every code deploy ships a PR-tested tree: the required build-and-test check runs the CI.slnf unit + architecture + bUnit tier (deploy.yml:220, :366) and branch protection enforces admins with strict up-to-date required checks (ADC PR #258), so the merged tree is the tree that check ran on, and a deploy that skips e2e-gate is still not untested (deploy.yml:812-816). Maturity 4: the E2E/axe layer is an enforced deploy gate. Held below 10: the browser layer (chromium E2E, axe scans, Core Web Vitals) is skipped on a backend/infra/script-only deploy (e2e-gate job :806 is ui-scoped at :817 and deploy accepts a skipped gate at :1196), there is no pixel visual regression, only chromium gates the deploy (:820; firefox/webkit stay advisory on the nightly, e2e.yml:144), and 12 snapshotted components is a small slice of the component surface. E2E #5 runs as an active test (SpeakerSelfServiceTests.cs:58, comment :49-58) with the original populator bug guarded by SessionIncludeChildrenRegressionTests.cs:27, and the create-form bUnit test (EventCreateTests.cs:17) narrows the state-coverage gap. Evidence: ComponentsSnapshotTests.cs:27,68,86,123,142; MMCA.ADC.CI.slnf:43,49,55; deploy.yml:220,:366,:806,:817,:820,:1159,:1196; e2e.yml:9,144; EventCreateTests.cs:17; SessionIncludeChildrenRegressionTests.cs:27 |
| 29 | Resilience & Business Continuity | 3 | 4 | 9 | 12/27 | Failure isolation (Polly retry/breaker as a fitness-enforced framework invariant), graceful degradation (outbox), geo-redundant PITR+LTR backups, defined RTO/RPO, SLO alerts, and smoke-gate auto-rollback are all present, and the rubric's #1 red flag is closed: the restore is drilled, repeatedly (infra/DISASTER-RECOVERY.md:212-217, six PITR proofs covering all four live databases, the newest a restore of ADC_Conference in 2.1 min against the 2 h RTO, produced by dr-drill.yml's weekly rotation under a 60-minute job timeout, dr-drill.yml:49). Graceful shutdown is CI-verified: the paired fault-injection test lives in Common (ResilienceCircuitBreakerFaultInjectionTests, consumed), and GracefulShutdownTests boots the real Gateway via WebApplicationFactory, requests a stop under a bounded ~20s token, and asserts the host drains (ApplicationStopping→ApplicationStopped) within the timeout (headless, in CI.slnf), so Implementation is 9. Maturity 4: a dr-freshness job in deploy.yml's deploy.needs fails the deploy if the latest dr-drill.yml run did not conclude success within an 8-day freshness window (covering the weekly cron), so the recovery proof blocks the deploy and the rubric's M4 ("enforced automatically by a CI gate") is met. Right-sized for the ~67-peak load: the gate reads the last drill's status via one Actions API call with no per-deploy restore cost, while the real PITR restore runs on dr-drill.yml's weekly cron. The Azure Monitor DR dashboard ships (sloWorkbook in main.bicep:772, embeds workbooks/adc-slo-workbook.json). Evidence: ADR-009 (RTO/RPO + single-region acceptance); .github/workflows/deploy.yml:844 (dr-freshness step, window-days: '8' at :853, in deploy.needs at :1159, the 8-day recovery-proof gate); .github/workflows/dr-drill.yml:31-33 (weekly cron, the real PITR restore the gate reads), :49 (the 60-minute job timeout, rationale :42-48); Tests/Hosts/MMCA.ADC.Gateway.Tests/GracefulShutdownTests.cs (a sealed subclass; the assertion body lives in MMCA.Common.Testing/GracefulShutdownTestsBase.cs); infra/main.bicep:984 (LTR on all 4 ADC_* DBs), :482 (the sloAlerts scheduled-query loop), :772 (SLO workbook) |
| 30 | Compliance, Privacy & Governance | 2 | 4 | 9 | 8/18 | Reconciles soft-delete with right-to-erasure exactly as the rubric demands: a real anonymize-in-place path, an export/access endpoint, bounded outbox/log retention, a PII inventory with purpose, lawful basis and retention per field (PRIVACY.md:225; session IP/UserAgent retention at :236) kept in step with the [Pii] markers by the CI-gated Tests/Architecture/MMCA.ADC.Architecture.Tests/Privacy/PiiInventoryTests.cs, and a PII fitness function enforcing the erasure obligation in CI. PRIVACY.md:53,61 states East US 2 apps / West US 2 SQL, matching the deployment, guarded by the DataResidencyTests fitness function so docs and main.bicep cannot drift. Evidence: ADR-005; Identity.Domain/Users/User.cs:510 (User : IAnonymizable; idempotent Anonymize in place); Tests/Integration/MMCA.ADC.Identity.IntegrationTests/Compliance/ErasureAndPiiLoggingTests.cs:55; DeleteUser/DeleteUserHandler.cs:47-49; Tests/Architecture/.../DataResidencyTests.cs |
| 31 | Cost Efficiency / FinOps | 2 | 4 | 8 | 8/16 | FinOps is a first-class, data-driven axis: right-sized Basic tiers matched to a measured ~67-peak load, a conference-day surge declared in IaC with a deploy-gating guard against an un-reverted one, a budget with spend alerts, tuned log retention + telemetry filtering, per-service cost tags, directly answering every criterion/red flag. Cost attribution splits by service: commonTags (infra/main.bicep:203) plus a service tag on each of the six container apps (:1705, :1952, :2105, :2240, :2430, :2593) and on the four per-service databases (:991), pinned by Tests/Architecture/MMCA.ADC.Architecture.Tests/Governance/CostTagConventionTests.cs:20,:39. dr-drill.yml:101-102 sweeps drill databases under always() so none is left at Basic-tier cost, and YARP Information-level log volume is cut to Warning (infra/main.bicep:294-302). Maturity 4: cost-guard.yml is a workflow_call reusable workflow (:19) invoked as a cost-guard job in deploy.yml's deploy.needs, so a deploy is blocked by an out-of-band scale-up (cost-guard.yml:120-121) or by a surge still live after its window (:116-118). The surge itself is declared in IaC: conferenceMode (infra/main.bicep:157) raises the front-door replicas and the SQL tier (:191-192, :995-997), and every deploy switches it from the CONFERENCE_MODE_UNTIL repository variable (deploy.yml:1430-1445), so the first deploy after the window is the revert (deploy.yml:793). The surge is reverted: az containerapp list -g acc-rg reads all six adc-prod-* apps at minReplicas 1 / maxReplicas 2, and CONFERENCE_MODE_UNTIL is no longer a repository variable. Held at 8: the revert waits on a deploy plus a manual variable delete, nothing schedules it, and the read-only guard runs weekly (cost-guard.yml:14, Mondays), so an expired surge can bill for up to a week before anything flags it; the rubric's "reversible scale events" criterion (ArchitectureEvaluationCriteria.md:827) is met for the surge and only partly for the revert. A daily cost-guard run is the 8→9 lever. Evidence: .github/workflows/cost-guard.yml (read-only surge-drift check, header :3-10, also workflow_call at :19: compares Container App maxReplicas against the baseline, :82, and SQL tier against Basic, :99, and reads the conference window at :63-68); .github/workflows/deploy.yml:783 (cost-guard job, in deploy.needs at :1159, condition :1192); infra/main.bicep:819 (monthly RG budget with 80%/100% alerts); load-test.yml:18 (k6 67 VUs = observed peak, on dispatch; the scheduled run uses 40, :75); infra/main.bicep:1942, :2095, :2230 (the three REST apps at maxReplicas 2), :2416 (Notification at minReplicas 1 / maxReplicas 2); infra/main.bicep:203, :991, :1705 (cost tags); infra/main.bicep:157, :191-192, deploy.yml:1430-1445 (conference mode) |
| 32 | Dependency & Supply-Chain | 2 | 4 | 9 | 8/18 | Central pinned versions, a deliberately licence-pinned MassTransit v8 (the exact 'accidental major bump reintroduces known-bad' red flag, defended with rationale), coordinated all-consumers-at-once upgrade, committed lock files and blocking supply-chain CI gates. ADC commits 67 packages.lock.json (RestorePackagesWithLockFile=true), and its vuln-audit + SBOM steps are blocking PR gates (in deploy's needs, run on pull_request), so transitive provenance is frozen and supply-chain governance is CI-enforced (maturity Optimized). CI restore runs --locked-mode in all four gating restore points (deploy.yml:297 build-and-test, :540 ai-eval-gate, :595 supply-chain, :708 integration-tests), so lock-file drift is tamper-enforced at restore. Five ImageSharp 3.1.12 advisories are suppressed by a dated, argued hold mirrored from MMCA.Common (Directory.Build.props:55-59, rationale :46-53; recorded in the backlog's Deliberate and accepted). Held below 10 because MassTransit v8 is pinned only transitively (inherited from Common, not in ADC's own props), no dependency-licence scan exists (the ImageSharp v3 licence pin is tracked by comment), and the five suppressions are accepted risk. maui-audit.yml (TD-18) covers the one graph the gating supply-chain job cannot see, because the MAUI head is excluded from MMCA.ADC.CI.slnf: a weekly suppress-aware dotnet list package --vulnerable --include-transitive over Source/Hosts/UI/MMCA.ADC.UI/MMCA.ADC.UI.csproj on net10.0-android (maui-audit.yml:52-53, cron :36) that exits 1 on any non-suppressed advisory (audit step :112, the failure exit at :160); scheduled and advisory, not a deploy gate, and the Apple TFMs stay uncovered on a Linux runner. Evidence: Directory.Build.props:31 (RestorePackagesWithLockFile); 67 committed packages.lock.json; deploy.yml:577 (the supply-chain job; audit gate through the shared Common composite action at :608, scoped to MMCA.ADC.CI.slnf at :610, no continue-on-error), :623 (SBOM gate, PR-only, through the shared composite action), :1159 (deploy needs [changes, supply-chain, cost-guard, freshness, e2e-gate, ai-eval-gate, foundation, build-images]); Directory.Packages.props:98-130 (MMCA.Common.* pinned lockstep at v1.244.0); nuget.config:8-19 (auditSources clears→nuget.org, packageSourceMapping MMCA.*→github) |
| 33 | Developer Experience & Inner Loop | 2 | 4 | 9 | 8/18 | The inner loop hits nearly every criterion: genuine one-command Aspire orchestration, a documented gitignored local.props cross-repo override, consistent analyzer/editorconfig tooling shared with CI, a fast CI solution filter, and a full getting-started README. Maturity 4: servicebus-emulator-smoke (cross-service-tests.yml:142) carries no continue-on-error under a header block that records it authoritative (:116) and forbids re-adding one (:124); no job in the file carries one. The cross-service-freshness step of the freshness job selects a run in which both cross-service and servicebus-emulator-smoke concluded success (deploy.yml:884, required jobs at :899-901), failing the deploy otherwise, with freshness in deploy.needs (:1159) and required at :1193: a red or absent Service Bus proof blocks the deploy, the same enforced-precondition standard as §29 and §31 (with the same dispatch-only, justification-mandatory break-glass). Implementation 9: broker parity is closed at the inner loop itself: ADC_BROKER=servicebus swaps RabbitMQ for AddServiceBusEmulatorBroker through Common's AddSelectedBroker (Source/Hosting/MMCA.ADC.AppHost/Program.cs:94, documented at :90-91), running the official emulator over the same transport production runs, via the pinned MMCA.Common.Aspire.Hosting package. Held below 10: the default local topology is RabbitMQ (Program.cs:75), so Service Bus is an opt-in parity profile rather than the default, and the prose hold no longer applies: README.md:93 and AGENTS.md:76 call the emulator tier authoritative, not advisory, and AGENTS.md:77 records that ADC has no AppHost smoke project (every e2e.yml run boots the real AppHost; the AppHost test base is covered by MMCA.Common's own AppHost tier). Evidence: README.md:1,74; cross-service-tests.yml:116,:124,:142; deploy.yml:884,:899-901,:1159,:1193; AppHost/Program.cs:75,:90-91,:94 and :126-393 (health-based WaitFor ordering, deliberate no-WaitFor reverse gRPC edge); Directory.Packages.props:113; CLAUDE.md (one-command dotnet run --project Source/Hosting/MMCA.ADC.AppHost → SQL/Redis/broker/MailDev + 4 services + Gateway + UI); local.props.template:5 (UseLocalMMCA) |
| 34 | Architecture Governance & Docs | 2 | 4 | 9 | 8/18 | Reference-quality governance: a maintained ADR set (canonical in ../adr/; range and count owned by ../adr/README.md) for every non-obvious decision, executable fitness functions (62 architecture-test classes, 64 non-obj .cs files, gating CI, including resilience/PII/concurrency/microservice-extraction/data-residency/cross-source-specification/UI-architecture/state-management/observability-pairing), with the rule bodies inherited from the shared MMCA.Common.Testing.Architecture library (whose shipped totals are owned by MMCA.Common/FACTS.md:51) plus 14 ADC-local test methods across six files (among them Ui/FormsConventionTests.cs:69,:108, Ui/InlineStyleTests.cs:21, Governance/CostTagConventionTests.cs:20,:39 and the six Domain/DeleteBehaviorConventionTests.cs facts), ADR-015, rather than prose; a living architecture map + scored backlog; and documented conventions linking to the enforcing tests. SpecificationConventionTests.cs:3-7 frames the Cosmos/SQLite layout as a forward safeguard, not the current layout. This scorecard is the canonical two-axis evaluation, versioned alongside the backlog, which is the single remediation + tech-debt ledger. Held at 9: the workspace architecture map (Docs/Architecture/ArchitecturalAnalysis.md, versioned in the private harness repo rather than in this repo) is stale, stating 13 packages at v1.82.0 (:99). Evidence: ../adr/README.md (the ADR index, owner of the range and count); Tests/Architecture/MMCA.ADC.Architecture.Tests/ (56 non-obj .cs files: 62 test classes across seven folders + AdcArchitectureMap.cs + GlobalUsings.cs); this ArchitectureScorecard.md (canonical two-axis) + RemediationBacklog.md (single ledger) + workspace ArchitecturalAnalysis.md |
Weighted column = Maturity·weight / Implementation·weight per row. The maturity gap is four categories at maturity 3 (§10, §12, §21, §22); every other category is at maturity 4, so implementation trails maturity only where a v2 criterion scores on implementation while maturity holds (§9, §11) or where a gate exists but the substance behind it is short (§22).
Scoring notes
- N/A (excluded from denominators): none. §16 AI-Native Application Architecture is scored because the AI session-scoring feature calls a model in production (the rubric's applicability rule,
ArchitectureEvaluationCriteria.md:473), so its weight 2 sits in both denominators and Σweight is 81. §27 Internationalization is scored under ADR-027 (which supersedes the single-locale ADR-011). - Rubric version 2 (ADR-110): §10 is Messaging & Integration Architecture at weight 3; §16 is AI-Native Application Architecture at weight 2.
- §32 weight = 2 (the default; raised to 3 only for the published framework MMCA.Common).
- The implementation index reads directly against 100%: a 10 is awardable when an implementation is almost perfect (every criterion met at reference quality, no red flags, at most trivial polish left), superseding the rubric's literal "nothing left to improve" wording. The denominators stay ×10.
- Backlog scheduling targets 9, not 10: ranking against 10 would put nearly every strong category in the band and drown the real gaps, so the 9→10 rung is recognition earned at re-score time. A 9→10 line under Top 5 risks or strengths reads as "what a future re-score would need to see", not as queued work.
- Framework pin: 19 of the 22 published
MMCA.Common.*packages, lockstep at v1.244.0 (Directory.Packages.props:98-130); theMMCA.Commonmetapackage,MMCA.Common.AI.OpenAIandMMCA.Common.Testing.Aspireare the three not consumed.
Cross-repo comparison
How this repo's quality relates to the other consumers (where the framework's quality propagates, where a repo diverges or under-uses it, and where a consumer does better than the shared baseline) is maintained once, for all three repos, in the workspace-internal Docs/Architecture/CrossRepoComparison.md (not published) (it is inherently cross-repo and would only drift if copied into each scorecard).