Architecture governance
MMCA.Common: Architecture Scorecard
Canonical, version-controlled scorecard for this repo: the single source of truth for MMCA.Common's architecture scores. Scored against the rubric at
ArchitectureEvaluationCriteria.md(rubric version 2, ADR-110); framework-wide facts (version, packages, ADR range, fitness counts) live inFACTS.md; the ranked open work lives in the remediation backlog. The cross-repo Common-vs-ADC-vs-Store narrative lives once in the workspace-internalDocs/Architecture/CrossRepoComparison.md(not published).
Evidence as of 2026-10-22 at v1.250.0, git HEAD 51c321fe (clean tree). Rubric version 2.
At a glance
- Maturity index = Σ(maturity×weight) ÷ Σ(weight×4) = 319 ÷ 328 = 97.3%
- Implementation index = Σ(impl×weight) ÷ Σ(weight×10) = 700 ÷ 820 = 85.4%
- Weaker axis: Implementation (execution quality), by ~11.9 points.
- Maturity band: 4 categories sit below Maturity 4 (§1, §17, §30 and §31, all at 3), 9 weighted gap points; see the backlog.
- Implementation band: 17 categories sit at Implementation 8 or below (§1, §4, §5, §10, §11, §12, §13, §17, §20, §23, §24, §27, §29, §30, §31, §33, §34), 38 weighted gap points against the scheduling target of 9; see the backlog.
MMCA.Common is strongest where its conventions are executable: 30 of 34 categories sit at Maturity 4, most of them on build-breaking gates (dual-enforced layering, fitness functions shipped as a package and re-run in every consumer, required cross-browser, accessibility, performance and consumer-canary checks, a CI-gated FACTS.md). The remaining gap sits on the Implementation axis, and most of its weight is structural rather than neglected: deployment execution, production SLOs and dashboards, realistic-volume load evidence, cost right-sizing and the consent and residency process belong to the consuming apps, which is why §17, §29, §30 and §31 hold at 8 or below. The two in-repo levers the backlog last named have shipped (client/server password parity on the shared change-password card, and the payload-budget check as a required context in both consumers), and Forms and Front-End Performance both hold at Implementation 8 on consumer-owned or residual gaps. Several other options are recorded as accepted rather than scheduled, and the remaining implementation-band categories, Messaging among them, have no lever identified yet.
Top 5 risks
FinOps execution and governance live downstream, §31 (impl 8, maturity 3; a documented accepted cap per the backlog's Deliberate and accepted section): the framework ships real cost levers (an OTel
Telemetry:TracesSampleRatiosampler knob, the outbox per-message log at Debug,OutboxPollFilterProcessor, and the default-ON probe-telemetry filter plus its span processor,Aspire/Extensions.Telemetry.cs:100,129, that keep health-probe traces and their SQL/Redis children out of ingestion), but right-sizing, reversible scale-events and per-service cost attribution are consumer/IaC concerns a library cannot perform. The one enforced cost convention is the shippedCostTagConventionTestsBaseattribution gate that ADC and Store subclass in CI; nothing enforces cost discipline at review time.- Remediation: The in-repo telemetry-cost half is done. What remains is structural: the consumer apps must demonstrate measured right-sizing and per-service cost attribution that the framework's knobs feed, and cost discipline would have to be enforced beyond the attribution tag (the pull-request template and
CONTRIBUTING.mdcarry no cost item). - Expected delta: +1 maturity on §31 (3→4, +2 weighted) needs cost enforced end to end by CI and review; 8→9 needs the consumer-side right-sizing and attribution evidence.
- Remediation: The in-repo telemetry-cost half is done. What remains is structural: the consumer apps must demonstrate measured right-sizing and per-service cost attribution that the framework's knobs feed, and cost discipline would have to be enforced beyond the attribution tag (the pull-request template and
Deployment gating is verified only downstream, §17 (maturity 3, impl 8, weight 2): a reference
samples/deploymentBicep/OIDC sample +DEPLOYMENT.mdship and are compile-checked on every pull request, but that job is not a required context, and a library cannot self-deploy, so real CD-to-Azure lives in consumers; keyless nuget.org publishing under ADR-053 and the reviewer-gatedreleaseenvironment harden the release identity, not the deploy axis.- Remediation: None scheduled in-repo. A scheduled Azure smoke-deploy of the
samples/deploymentsample is recorded as accepted rather than adopted (new Azure credentials, ongoing spend and a new CI failure path for a library, while ADC and Store prove the same Bicep/OIDC path in production; see the backlog's Deliberate and accepted). - Expected delta: +1 impl on §17 (+2 weighted), and the maturity rung, remain available downstream.
- Remediation: None scheduled in-repo. A scheduled Azure smoke-deploy of the
Compliance governance is consumer-owned; the in-repo guard verifies a mechanism, not the category, §30 (impl 8, maturity 3, weight 2): the headline §30 red flags are closed in-framework (the
IAnonymizableerasure extension point, AES-256-GCM at rest, and the shipped, testedPiiRedactormasking[Pii]members before logs/telemetry), andPiiErasureContractFitnessTestsforces a[Pii]type throughPiiRedactor+IAnonymizable.PiiConventionTestsadds a non-vacuous marking rule (Governance/PiiConventionTests.cs:10-14),PRIVACY.mdships in-repo, and the CSV export is fail-closed (EntityControllerBase.cs:231,272). The governing process (consent capture, lawful basis, residency verification) is delegated to consumers: consent has zero matches in Domain, Application and Infrastructure, andDataResidencyTestsBasehas only its self-test probe in-repo, so the category as a whole is not automatically enforced.- Remediation: The in-repo mechanism is done; further maturity movement needs the consumer apps to carry the personal-data inventory + DSAR/consent/residency process the framework's
[Pii]/IAnonymizableextension points feed (ADR-005). - Expected delta: +1 maturity on §30 (3→4, +2 weighted) is a consumer-side governance lift, not an in-repo one; the framework half is complete.
- Remediation: The in-repo mechanism is done; further maturity movement needs the consumer apps to carry the personal-data inventory + DSAR/consent/residency process the framework's
Security holds at Implementation 8 on criteria that are unmet by decision, §11 (maturity 4, impl 8, weight 3, implPriority 3): authentication, authorization, rate limiting, brute-force protection, PBKDF2 hashing and security headers are strong and centralized, but the rubric-v2 written threat model is absent repo-wide and is recorded as not scheduled work by user decision, authorization is RBAC with capability indirection plus opt-in ownership rather than a resource/attribute policy engine. Two internal trust gaps sit beside it: the broker consumer restores user id, roles and tenant from unsigned
MMCA-*message headers (Infrastructure/Messaging/Consumers/ConsumerOriginRestore.cs:44,53,Shared/Messaging/MessageHeaders.cs:31), so any party able to publish to the broker can assert roles (signing them is recorded as accepted, not scheduled); and in-cluster gRPC forwards the user JWT over deliberate, documented cleartext h2c (Grpc/DependencyInjection.cs:54,97-99), where the shipped service identity is an opt-in app-layer HS256 token over one key shared by every service (Grpc/Authentication/ServiceTokenServiceCollectionExtensions.cs:35,63,85,144,ServiceTokenSettings.cs:13,30), adopted by Store and not by ADC. Tenant resolution defaults to claim-only (Infrastructure/Persistence/Tenancy/TenancySettings.cs:61-62), with the header an explicit opt-in (:18-19) and a shippedTenantResolutionTestsBase(Source/Hosting/MMCA.Common.Testing/Conformance/TenantResolutionTestsBase.cs:27).SECURITY.md:83-86documents the shippedAddCommonKeyVaultConfigurationhelper (a no-op unlessKeyVault:Uriis set) and the sample's Key Vault URL plus managed-identity binding.- Remediation: Make the in-cluster service identity per-service or transport-level rather than one shared signing key. Signing the broker origin headers and the written threat model stay accepted, unscheduled gaps.
- Expected delta: The service-identity lever protects Implementation 8; 8→9 (+3 weighted) would also need the threat model and a resource/attribute policy engine.
Production continuity evidence is consumer-owned, §29 (maturity 4, impl 8, weight 3, implPriority 3): failure isolation is fitness-enforced and the in-repo restore drill is a build gate with a measured baseline, but the framework's own guide records production RTO/RPO, restores and SLO alerting as the consumer's (
Website/docs-src/guides/common-RESILIENCE.md:5,31), with the SLO section an unfilled template (:95).- Remediation: Tested restores against real cloud backups, RTO/RPO per service and measured production SLOs in the consumer IaC; nothing further is schedulable in-repo.
- Expected delta: +1 impl on §29 (+3 weighted) is a consumer-side lift.
Top 5 strengths
Dual-enforced Clean Architecture dependency rule (compile-time + fitness functions), §3 (impl 9):
Source/Build/MMCA.Common.LayerEnforcement.targets:1-90fails the build on layer violation (BeforeTargets ResolveProjectReferences);LayerDependencyTests.cs:12-58andDomainPurityTests.cs:13-26re-enforce Shared→Domain→Application→Infra→API/UI and Domain framework-freedom via NetArchTest. Reinforced by §34 (impl 8).- Remediation: Preserve by keeping both gates wired into CI and adding a regression test whenever a new layer/package is introduced; document the targets file in CLAUDE.md. To push the residual point, narrow the broader base-controller/UoW surfaces noted in §1.
- Expected delta: Preserves impl 9 on §3; protects §34 from regression.
Reference-quality test strategy (define-once fitness functions + an enforced coverage gate), §14 (impl 9): a right-shaped pyramid (~2,254
[Fact]/[Theory]) with executable architecture fitness functions (LayerDependency, DomainPurity, MicroserviceExtraction, DependencyVersion, PiiConvention, PiiErasureContract, AggregateConvention, EventVersioningConvention, FrameworkSanity, Specification, SliceCohesion) authored once in the shippedMMCA.Common.Testing.Architecturepackage (157 methods across 63 abstract bases,FACTS.md:81) and re-run identically across all three repos (ADR-015), a zero-discovery min-tests guard, a shipped reusable E2E package, and a 68.3% line-coverage floor that fails the build (ci.yml:409, measured ~70.3%).- Remediation: Preserve by keeping the floor green and ratcheting it upward over releases; add mutation testing on the Core tier to reach a 10.
- Expected delta: Holds §14 at impl 9; mutation testing on the Core tier is the remaining path toward 10; protects §28.
Supply-chain hardening with a license-pin build gate, a blocking SBOM, and Dependabot held-package guards, §32 (impl 9, weight 3):
Directory.Build.props:8-10(lock files, NuGetAudit, audit=all);Directory.Packages.props:54-56(MassTransit 8.5.5 pinned with documented v9 commercial-license crash hazard, ADR-016);DependencyVersionTestsBase.cs:24-37fails the build if any MassTransit package goes major≥9; 26packages.lock.json; CycloneDX SBOM a hard release gate plus signed build-provenance attestations (release.yml:98,107); and a committed.github/dependabot.yml:24-35withsemver-majorignores on every held package, so the pin discipline lives in config as well as the test.- Remediation: Automate the coordinated consumer version sweep (currently policy) to close the one remaining minor gap.
- Expected delta: Automating the sweep nudges §32 toward fully exemplary; preserves impl 9.
Centralized, alg-pinned security with shipped anti-automation defenses and PBKDF2 hashing, §11 (impl 8):
WebApplicationBuilderExtensions.Authentication.cs:238,254pins ValidAlgorithms per RS256/HS256 (defeats alg-confusion) with full issuer/audience/lifetime/signing-key validation,:109extends the RS256 pin to the JWKS-forwarded path;WebApplicationBuilderExtensions.RateLimiting.cs:46rate-limit bypass for health/.well-known/grpc;LoginProtectionService.cs:50-56,74-82adds exponential brute-force lockout + per-IP registration limit (ADR-029);PasswordHasher.cs:9,53hashes with PBKDF2-HMAC-SHA512 (600k iterations, PBKDF2-only,FixedTimeEquals, ADR-032): all returningResult.Failurerather than throwing.- Remediation: Keep the CI vulnerable-package gate green.
- Expected delta: Preserves impl 8; the threat model that 8→9 needs is an accepted, unscheduled gap (see risk 4).
Architecture-test-enforced microservices extraction boundaries + event versioning, §7 (maturity 4, impl 9): ADR-006 (database-per-service), transport-agnostic
IMessageBus.cs:28, and the sharedRules/Layering/ArchitectureRules.Transport.cs:19rule (subclassed throughBases/Layering/MicroserviceExtractionTestsBase.cs:13inMicroserviceExtractionTests.cs:11, run atci.yml:163under a required check) forbidding Application/Domain/Shared→MassTransit and Grpc→Domain/Application/Infrastructure. Reinforced by §6'sBaseIntegrationEvent.SchemaVersion(ADR-010) +EventVersioningConventionTests, so cross-service contracts carry an explicit version, and by ADR-090 upcasting for retired contracts. The rubric-v2 modernization-patterns criterion is met in shipped docs (Grpc/DependencyInjection.cs:21,71,78,007-grpc-extraction.md:23,008-service-extraction-topology.md:41).- Remediation: Preserve the naming by keeping the ACL and Strangler Fig wording in the Grpc XML docs and in ADR-007/008 whenever either is revised (no drift gate watches the naming itself); to push the residual point, demonstrate one end-to-end lift-out (the Notifications module) in a sample to convert "extractable by design" into "extracted and proven".
- Expected delta: 9→10 on §7 needs a proven extraction, which is structurally consumer-side.
Scorecard
| # | Category | Weight | Maturity (0-4) | Impl (0-10) | Weighted (M·w / I·w) | Evidence / Notes |
|---|---|---|---|---|---|---|
| 1 | SOLID Principles | 3 | 3 | 8 | 9/24 | The principles are applied throughout; SRP, DIP and the throw half of LSP are enforced automatically. DIP runs on two gates: the compile-time layer targets imported for every package (Directory.Build.props:166) and the NetArchTest layer, purity and clock-read subclasses (Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/LayerDependencyTests.cs:9, Domain/ClockReadTests.cs:14), with TimeProvider injected instead of an ambient clock (TokenService.cs:74). OCP holds where the sealed decorator pipeline and the permission registry (IPermissionRegistry.cs:14) are the change-points; ISP shows in the repository role split into IEntityReader, IEntityQuerier, IReadRepository and IWriteRepository (Application/Interfaces/Infrastructure/Persistence/IRepository.cs:332); Result<T> composes failures without exceptions (Shared/Abstractions/Result.cs:276, Map). SRP runs on a ratcheted constructor-dependency ceiling that Common subclasses (Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/ConstructorDependencies/FrameworkConstructorDependencyTests.cs:25; services ceiling 7 at :33, controllers 5 at :37, handlers 7 at :40), over a base that is itself tested on a fat and a thin fixture (Cqrs/ConstructorDependencyCountTestsBaseTests.cs:14), inside the required CI test run (ci.yml:163). LSP is gated on its throw half: SubstitutabilityTestsBase reads the IL of every override and interface implementation and fails on NotSupportedException or NotImplementedException (Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/SubstitutabilityTestsBase.cs:34), subclassed with no exclusions at Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Substitutability/CommonSubstitutabilityTests.cs:10; MMCA.Common.UI.Maui sits outside CommonArchitectureMap (CommonArchitectureMap.cs:9), so the gate does not scan it. Maturity 3 (not 4): OCP and ISP have no automated gate in Common, the type-sniffing half of LSP is ungated with live instances (unitOfWork is IUniqueConstraintViolationDetector at ChildEntityHandlerBase.cs:139, an as ... ?? new SqlServerUniqueConstraintViolationDetector() fallback at IdentityModuleDbSeederBase.cs:162), and S107 (too many parameters) and S1200 (too many dependencies) sit at severity suggestion (.editorconfig:478,492). Implementation 8 (not 9): the constructor and engine red flags are absent from shipped code (Application/Auth/AuthenticationServiceBase.cs:64-71 takes 7 parameters, Infrastructure/Persistence/Outbox/Processing/OutboxProcessor.cs:58-64 and Infrastructure/Persistence/InternalCommands/Processing/InternalCommandProcessor.cs:43-49 take 6; engine-specific behavior routes through one IDataSourceEngine strategy, Infrastructure/Persistence/DataSources/Engines/IDataSourceEngine.cs:18, selected by DataSourceEngines.For; the Application port exposes the engine-neutral RequestExplicitKeyInsert(), Application/Interfaces/Infrastructure/Persistence/IUnitOfWork.cs:45; EFRawSqlQueryExecutor.cs throws no NotSupportedException; the NotSupportedException hits in 19 Source files are catch filters, with the two real throws in non-override helpers, DbSeeder.cs:38 and KeysetQueryBuilder.cs:330), but the type-sniffing above stands, ApplicationDbContext takes an IServiceProvider (ApplicationDbContext.cs:48) and resolves about 13 collaborators through GetService calls, which hides them from the SRP ceiling, unique-violation classification uses one SqlServer-named detector for every engine outside the IDataSourceEngine strategy, and a few classes are very large (OutboxProcessor 1181 lines, ApplicationDbContext 989, AuthenticationServiceBase 641). The lever to 9 is not yet identified; an ISP member-count ceiling is recorded as accepted, not adopted. Evidence: Application/DependencyInjection.cs:134-148 (Scrutor TryDecorate, innermost-first, execution order FeatureGate, Authorization, Logging, Caching, Validating, Timeout, Transactional, Handler); Infrastructure/Persistence/DbContexts/ApplicationDbContext.cs:48 (IServiceProvider injected into the base context) |
| 2 | Design Patterns | 2 | 4 | 9 | 8/18 | Idiomatic, problem-driven: Result over exceptions, Repository/UoW, Specification with composition (including InlineSpecification and the polyglot CrossSourceSpecification), Decorator pipeline, entity Factory+Result, Outbox, Strategy: consistently named; no pattern-theater or anemic manager/util classes; vetted libs (Scrutor/MassTransit/FluentValidation). Evidence: Shared/Abstractions/Result.cs:165/181/194 (Match/Map/BindAsync); Domain/Specifications/Specification.cs:62/88/114 (And/Or/Not expression-tree composition, EF-translatable) + :45 (InlineSpecification); Application/Specifications/CrossSourceSpecification.cs:39; Application/DependencyInjection.cs:88-102 |
| 3 | Clean Architecture | 3 | 4 | 9 | 12/27 | Dependency rule points strictly inward, enforced twice (MSBuild targets + NetArchTest); domain verified framework-free; Application defines ports, Infrastructure implements adapters. No circular refs / shared-kernel dump. Held at 9 (not 10) by three criteria-level residuals: generic CRUD primitives in Application/UseCases/Crud/ only partly meet the use-case-centric criterion (UseCases/Crud/CreateEntityHandler.cs:28-31), the IUnitOfWork port is a five-member store-shaped surface that leaks SQL Server SET IDENTITY_INSERT semantics through RequestIdentityInsert (Source/Core/MMCA.Common.Application/Interfaces/Infrastructure/Persistence/IUnitOfWork.cs:38-45, the same residual the §1 row names), and seven of eighteen Source packages sit in neither the compile-time nor the NetArchTest gate (Tests/Architecture/MMCA.Common.Architecture.Tests/CommonArchitectureMap.cs:19-28). Evidence: Source/Build/MMCA.Common.LayerEnforcement.targets:20 (nine per-package compile-time guards, BeforeTargets ResolveProjectReferences, in a 187-line file; imported Directory.Build.props:166 for every MMCA.Common* Source project); Tests/Architecture/.../Layering/LayerDependencyTests.cs:9 and .../Layering/DomainPurityTests.cs:9 (13-line sealed subclasses; the 14 layer rules and the purity ban list live in the shared bases, Testing.Architecture/Rules/Layering/ArchitectureRules.Purity.cs:9: AspNetCore/EF Core/Serilog/AutoMapper/Newtonsoft/FluentValidation/Scrutor/MudBlazor/Polly/Redis); Application/DependencyInjection.Crud.cs (the CRUD registrations as a partial class) |
| 4 | Domain-Driven Design | 3 | 4 | 8 | 12/24 | Strong tactical DDD: aggregates with enforced invariants + domain events, immutable VOs with Result factories, identifier aliases vs primitive obsession, and an AggregateConventionTests fitness fn pinning Create→Result<T>. Domain purity: UserNotification.MarkAsRead(DateTime readOnUtc) takes an explicit UTC timestamp, with TimeProvider injected at the handler edge. The one domain DateTime.UtcNow, BaseDomainEvent.DateOccurred, is a deliberate, documented choice: a domain event's occurrence instant is the moment the aggregate raises it, the correct event-sourcing/audit semantic (four domain tests enforce it); moving the stamp to the persistence boundary would turn occurrence-time into persist-time. Held at 8 (not 9) by two open criteria: strategic DDD (bounded contexts, ubiquitous language) is realized downstream, since the Domain layer holds one aggregate family (Notifications) plus auth support types; and the tenant identifier is a plain string by deliberate decision rather than a strong type, although ADR-073 records the tenancy model and Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/EntityModel/TenantEntityConventionTests.cs:20 gates the TenantId marker. Common ships 11 domain fitness bases and runs the entity and immutability ones itself (Domain/EntityModel/EntityConventionTests.cs:12, ImmutabilityTests.cs:11) in the merge-gated unit tier. The lever to 9 is not yet identified. Evidence: Domain/Entities/AuditableAggregateRootEntity.cs:13-105; Domain/Notifications/.../UserNotification.cs:53-58 (explicit-timestamp injection point); Domain/DomainEvents/BaseDomainEvent.cs (deliberate occurrence-time default, see <remarks>); Shared/ValueObjects/Financial/Money.cs, Contact/{Address,Email,PhoneNumber}.cs, Time/DateRange.cs; Tests/Architecture/.../AggregateConventionTests.cs (a 13-line subclass; the four pinned rules, DomainExposesAggregateRoots / AggregateRootsHaveResultFactory / DomainAggregateRootsHaveNoPublicConstructors / DomainFactoriesReturnResult, live in Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/AggregateConventionTestsBase.cs:14-24), run in the CI unit tier (MMCA.Common.slnx:56, ci.yml:163) |
| 5 | Vertical Slice Architecture | 2 | 4 | 8 | 8/16 | Slice cohesion is machine-enforced: a SliceCohesionTestsBase fitness function (shared MMCA.Common.Testing.Architecture) fails the build if a handler/validator is stranded from its same-assembly command/query contract, which meets the rubric's maturity-4 "enforced automatically by tests/CI" bar. Both slice rules scan every Application class including abstract bases (Rules/Cqrs/ArchitectureRules.Slices.cs:35,65 over the RuleHelpers.cs:44 Classes helper), with three compiled-fixture self-tests (Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/SliceCohesionFitnessTests.cs:21,32,43: stranded base flagged, co-located base not flagged, generic-contract base skipped). Where features exist they are clean vertical slices (Feature/UseCases/{Action}/ co-locating command/query + handler + validator + DTO) with cross-cutting in the pipeline. A filesystem fitness rule caps every folder at 12 direct code files (Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/FolderWidthTests.cs:18, in the PR-gated slnx at MMCA.Common.slnx:59). Held at 8 (not 9): Common is an SDK with no business use-case slices of its own and its primitives are deliberately horizontal (correct for infra), so slice cohesion is proven in the consumer repos; the rule checks only a concrete same-assembly contract, and handler bases that implement ICommandHandler/IQueryHandler over a generic parameter stay exempt by design (ArchitectureRules.Slices.cs:31, ChangePasswordHandlerBase.cs:41). The lever to 9 is not yet identified. Evidence: Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.Slices.cs:35,65,110 + Bases/SliceCohesionTestsBase.cs; Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/SliceCohesionTests.cs:10; Application/Notifications/PushNotifications/UseCases/Send/; .../UserNotifications/UseCases/{MarkRead,MarkAllRead,GetInbox,GetUnreadCount}/; Application/Users/UseCases/ (abstract handler-base slices) |
| 6 | CQRS & Event-Driven | 2 | 4 | 9 | 8/18 | Real command/query separation with an enforced, documented decorator pipeline; atomic outbox persist + dual dispatch; genuinely idempotent inbox consumer (dedup by MessageId via IInboxStore, documented in ADR-021 as the broker-consume sibling of ADR-003's outbox + ADR-017's HTTP-edge idempotency). Event-schema versioning: BaseIntegrationEvent.SchemaVersion => 1 (ADR-010) + an EventVersioningConventionTests fitness gate, and the ADR-090 upcaster pipeline, Application/Interfaces/Events/IEventUpcaster.cs:28 (typed IEventUpcaster<TSource,TTarget>), with the dispatcher upcasting to terminal at DomainEventDispatcher.cs:62 and two fitness rules policing upcaster shape at Testing.Architecture/Rules/Contracts/ArchitectureRules.Upcasters.cs:12,28 inside the CI-gated EventConventionTestsBase (Bases/Contracts/EventConventionTestsBase.cs:23). Held below 10 because read models are DTO projections and two criteria sit short of reference quality. Evidence: Domain/DomainEvents/BaseIntegrationEvent.cs:32 (the SchemaVersion member); Infrastructure/Persistence/Outbox/OutboxProcessor.cs (drain :221, dead-letter :282-283, retry :305-308); Persistence/Inbox/EfInboxStore.cs:29 |
| 7 | Microservices Readiness | 3 | 4 | 9 | 12/27 | Bounded-context modules extractable without rewrites (ADR-008), real data ownership per service (ADR-006), async events + gRPC only where sync needed, resilience policies, versioned contracts, trace propagation. Extraction boundaries enforced by fitness functions, not convention. No distributed-monolith/shared-table flags. The rubric-v2 modernization-patterns criterion (ArchitectureEvaluationCriteria.md:263) is met: the Grpc package's class remarks and AddTypedGrpcClient XML docs name the hand-written adapter as the consuming module's Anti-Corruption Layer and the monolith-to-service move as the Strangler Fig route (Grpc/DependencyInjection.cs:21,71,78, CHANGELOG.md:553), ADR-007 names the *.Contracts adapter as the ACL (007-grpc-extraction.md:23), ADR-008 spells out the three-step sequence (stand the new host up beside the monolith, move one route prefix at the Gateway, retire the old host, 008-service-extraction-topology.md:11,41) while recording that ADC itself took the one-step cutover, and the package README carries the same naming (README.md:71). Held at 9, not 10: the naming documents an existing convention with no drift gate on the naming itself, extraction has only been exercised as a one-step cutover in a consumer, and independent-deployability pipelines are structurally consumer-side for a library. Maturity 4: the boundary is a required-check merge gate, not review enforcement. Evidence: ADR-006; Application/Messaging/IMessageBus.cs:28 (InProcess vs Broker impls); Testing.Architecture/Rules/Layering/ArchitectureRules.Transport.cs:19 (TransportDoesNotLeakIntoCoreLayers, rule body in the shared package) + Bases/Layering/MicroserviceExtractionTestsBase.cs:12, subclassed at Tests/Architecture/.../Layering/MicroserviceExtractionTests.cs:11 (in MMCA.Common.slnx:56, run at ci.yml:163); Domain/Attributes/EventNameAttribute.cs:22 (versioned contract identity) + Infrastructure/Messaging/Consumers/UpcastingIntegrationEventConsumer.cs:32 (ADR-090 upcasting); Infrastructure/Messaging/Consumers/IntegrationEventConsumer.cs:54 (inbox row staged in the handler's own unit of work); Grpc/DependencyInjection.cs:127-136 (attempt + total timeout, retry budget, circuit breaker); Application/Modules/ModuleLoader.cs:264 (Kahn ordering) and :108 (stub registrations keep cross-module interfaces resolvable in an extracted single-module host) |
| 8 | Data Architecture | 3 | 4 | 9 | 12/27 | Audit fields stamped centrally via AuditSaveChangesInterceptor (not per-handler), soft-delete + global filters via reflection over IAuditableEntity, optimistic concurrency tokens on every auditable entity, batch-by-IDs eager-loading vs N+1, per-service migrations/isolation. Polyglot persistence (ADR-018): three storage engines behind one entity model, engine chosen per-entity by [UseDataSource] on an engine shim base, cross-engine relationships auto-degraded (CrossDataSourceDegradeConvention), an engine-portable CrossSourceSpecification.BuildAsync, and a SpecificationsDoNotNavigateToOtherEntities build gate, proven by unit/integration tests. Migrations are applied in CI: the required Consumer source build (Helpdesk) job applies a real consumer's committed EF migrations to an ephemeral SQL Server via dotnet ef database update built against the PR's framework source, with the outcome asserted rather than exit-code-trusted (__EFMigrationsHistory row count plus Tickets.Ticket and the framework-owned dbo.OutboxMessages must exist; ci.yml:589,612, no continue-on-error on the job), plus a DB-free apply proof in the CI-gated unit tier (MigrationApplyProofTests.cs:95: a committed migration applied through DbContextFactory.MigrateAsync, history row + created table asserted; both projects in MMCA.Common.slnx:42-43) and a production schema-drift guard (the None strategy validates no pending migrations on any source and throws if behind, DatabaseInitializationExtensions.cs:124). Cascade soft-delete is machine-gated: an IL-level fitness rule (an aggregate root owning a child collection must cascade in its Delete() override, ArchitectureRules.CascadeSoftDelete.cs:35) plus a hard-delete ban outside seven individually named, reviewed framework types (SoftDeleteEnforcementTests.cs:24). Multi-tenant row isolation ships as a named Tenant global filter with a TenantSaveChangesInterceptor ordered between audit and outbox stamping (ApplicationDbContext.cs:306-308, interceptor order audit → tenant → domain-event → audit-trail at :308,322, filters applied centrally at :419), with an opt-in entity change trail (:320-322), a SoftDeleteUniqueIndexConvention (:393), an opt-in DeleteChildren<TChild,TChildId> cascade helper (AuditableAggregateRootEntity.cs:273-292, unit-tested) with a DeleteEntityHandler.Includes/AsTracking eager-load extension point (DeleteEntityHandler.cs:57,63), the shared RawQueryable/Concurrency convention rules wired into the CI-gated arch tier (RawQueryableConventionTests.cs:13-15, ConcurrencyConventionTests.cs:12-14; the concurrency run finds no candidate in Common's own map, so in-repo it is a ratchet, ConcurrencyConventionTests.cs:9), and keyset (seek) paging ordered by (sortKey, Id) on read paths (KeysetQueryBuilder.cs:22). Held at 9 (not 10): in-repo the cascade rule is a ratchet (Common's Source declares no child-bearing aggregate, CascadeSoftDeleteConventionTests.cs:8-12) and the default Delete() flips IsDeleted on the single entity only (AuditableBaseEntity.cs:67); cross-source consistency is best-effort sequential (no 2PC, ADR-006), with a commit-phase failure surfaced as an explicit ambiguity rather than retried (TransactionCommitAmbiguousException with a per-source committed/rolled-back breakdown, DbContextFactory.cs:776); ADR-018's non-SQL engines stay deliberately latent (no production entity on them); and the public IRepository.ExecuteDeleteAsync bypasses soft-delete, audit and domain events, with callers that reach it through that interface outside the hard-delete gate (IRepository.cs:455, ArchitectureRules.SoftDelete.cs:51). The lever from 9 to 10 is two rubric-v2 criteria: no recorded decision covers separating analytical reads from OLTP for the framework (ArchitectureEvaluationCriteria.md:289 accepts a documented decision that none exist), and the expand/contract schema-evolution gate runs only consumer-side, because MMCA.Common ships no consumer-facing migrations of its own (../adr/057-expand-contract-schema-evolution-gate.md:116, criterion ArchitectureEvaluationCriteria.md:288). Evidence: Infrastructure/Persistence/DbContexts/ApplicationDbContext.cs:454 (ApplySoftDeleteFilters, a named filter, applied at :419), :589-606 (ConfigureConcurrencyTokens, IsRowVersion :602 / IsConcurrencyToken :606), :298,308 (interceptor audit stamping registered from DI); Infrastructure/Persistence/Configuration/EntityTypeConfiguration/EntityTypeConfiguration.cs + EntityTypeConfiguration{SQLServer,Cosmos,Sqlite}.cs; Infrastructure/Persistence/Conventions/CrossDataSourceDegradeConvention.cs:33-35; Application/Specifications/CrossSourceSpecification.cs:39 |
| 9 | API & Contract Design | 2 | 4 | 9 | 8/18 | Consistent RFC 9457 ProblemDetails with centralized status mapping, header-based versioning with a backward-compatible v1.0 default, uniform pagination/filtering/sorting over the generic EntityControllerBase query contract (ADR-034), edge validation, DTO decoupling by ADR, OpenAPI generation (AddCommonOpenApi()/MapCommonOpenApi() over built-in Microsoft.AspNetCore.OpenApi, serving /openapi/v1.json outside Production) plus the opt-in Scalar UI (MapCommonScalarUi(), non-Production only). Request idempotency at the inbound edge is ADR-documented (ADR-017). Contract discipline is enforced automatically in-repo on both axes (the rubric's Maturity-4 bar): (a) an OpenAPI committed-baseline diff normalizes the document a probe host generates through the real AddCommonOpenApi/MapCommonOpenApi pipeline and fails on any change against the committed openapi-baseline.v1.json until it is regenerated deliberately in the same pull request (MMCA_UPDATE_OPENAPI_BASELINE=1), scoped to the framework-owned surface (the single v1 document, the unbound-route-token backfill, the generated ProblemDetails error schema) because each consumer's concrete API surface is guarded by that host's own snapshot tier (e.g. ADC's OpenApiContractTests); and (b) a dedicated [ServiceContract] purity fitness rule fails the build when a marked contract type reaches into a mapped Domain/Application/Infrastructure namespace, naming the offending type; it is attribute-driven, so it runs as a ratchet in Common (no type carries the attribute here) and bites in the consumers. Both projects sit in MMCA.Common.slnx:46,59, so both run in the CI-gated unit tier. Held at Implementation 9: the baseline covers the framework-owned surface only, the purity rule's in-repo run is vacuous until a *.Contracts type is marked, only a single v1 OpenAPI document is served despite header versioning (OpenApiEndpointExtensions.cs:28,65), and integration events carry a frozen-snapshot gate (IntegrationEventContractTestsBase.cs:35-53) but no AsyncAPI or equivalent published contract. Evidence: Tests/Presentation/MMCA.Common.API.Tests/OpenApi/OpenApiBaselineTests.cs:45-76 (the diff gate) + OpenApiProbeHost.cs:20 + openapi-baseline.v1.json; Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Contracts.cs:32 + Bases/Layering/ServiceContractPurityTestsBase.cs:24-26, subclassed at Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/ServiceContractPurityTests.cs:11; API/Startup/Endpoints/OpenApiEndpointExtensions.cs:60,89 (MapCommonOpenApi + MapCommonScalarUi, with the two-level guard); API/Startup/WebApplicationBuilderExtensions.cs:39-63 (header api-version with the v1.0 default) and :101-115 (AddCommonOpenApi); Source/Core/MMCA.Common.Shared/Abstractions/ServiceContractAttribute.cs (invariant + ADR-015 purity rules); API/Idempotency/IdempotencyFilter.cs (ADR-017); API/Middleware/ErrorHttpMapping.cs:20-32 |
| 10 | Messaging & Integration Architecture | 3 | 4 | 8 | 12/24 | Broker topology is by ADR and configuration: Infrastructure/Messaging/BrokerEventBus.cs:75 writes integration events to the outbox and signals the processor instead of dispatching in-process (throwing loudly when a source does not support an outbox), and BrokerMessageBus.cs:9 publishes over MassTransit to RabbitMQ in dev or Azure Service Bus in production (ADR-008/ADR-066). Delivery semantics are per consumer and layered: every receive endpoint gets exponential UseMessageRetry (Infrastructure/DependencyInjection.Messaging.cs:292, Service Bus :332) above second-level UseDelayedRedelivery for outages measured in minutes to hours (:288, Service Bus :329), and FaultIntegrationEventConsumer.cs:8 turns exhausted-retry error-queue rows into a structured Error log plus a broker.fault.count metric without re-throwing. Retention and replay are explicit: OutboxCleanupService.cs:19 purges processed rows past RetentionDays while holding dead-lettered rows for DeadLetterRetentionDays, and IOutboxAdministration.cs:51 exposes ReplayDeadLettersAsync. Consumers are idempotent by construction: EfInboxStore.cs:43 stages the inbox row in the handler's own transaction with a unique MessageId index absorbing concurrent duplicate deliveries (unit-covered by EfInboxStoreTests.cs), and UpcastingIntegrationEventConsumer.cs:13 drains retired contracts through IEventUpcasterRegistry to the terminal contract with dedup keyed on the original message id (ADR-090). Contract evolution is fitness-enforced (EventVersioningConventionTests.cs and EventUpcasterFitnessTests.cs, both inside MMCA.Common.slnx and run on every pull request, ci.yml:15), and the gateway stays routing plus cross-cutting behavior rather than a second business layer (ADR-008 as amended by ADR-088/ADR-089). Held at 8: RabbitMQ delayed redelivery is opt-in (EnableDelayedRedelivery, default off) with no startup warning when it is off (DependencyInjection.Messaging.cs:283); zero or negative redelivery intervals are silently dropped rather than rejected (:381); the fault consumer logs and counts but never replays, and broker.fault.count drives no shipped alert, with the replay runbooks living only in the consumers (FaultIntegrationEventConsumer.cs:21, ../adr/087-broker-poison-message-handling.md:349); no CI job runs a live broker (ServiceBusEmulatorBrokerTests.cs:11 checks the app model only); and sagas and process managers are a documented deferral (ADR-086), with choreographed compensation plus a persisted marker and a periodic reconciliation sweep (ADR-054) as the accepted interim rather than a shipped orchestrator. Related cross-cutting evidence (scored under §5/§6/§9/§12/§17/§29): validation, logging, transactions, caching and feature-gating are handled once in the decorator pipeline; feature flags are ADR-documented (ADR-031: Microsoft.FeatureManagement on both the controller [FeatureGate] surface and the outermost IFeatureGated CQRS decorator, disabled = 404); configuration is strongly typed with ValidateOnStart on seven options types; the caching substrate is ADR-documented (ADR-026: application code depends only on ICacheService, and AddCaching() binds DistributedCacheService when a real IDistributedCache is present, else MemoryCacheService, with a short default TTL backstopping best-effort prefix invalidation); IdempotencyFilter resolves an IDistributedLock (RedisDistributedLock, SET NX PX acquire + compare-and-delete Lua release, whenever an IConnectionMultiplexer is present) and falls back to a striped per-process semaphore only when a host registers none, with the shipped cache exercised against a real Redis in CI (ADR-017). Remaining cross-cutting residuals: prefix eviction stays TTL-only with no multiplexer (warn-logged), HttpResilienceDefaults values are compile-time constants rather than configuration-bound, and RedisDistributedLock itself is mock-tested only in the unit tier. Evidence: Application/UseCases/Decorators/* (10 decorators); Application/Interfaces/ICacheService.cs; Infrastructure/DependencyInjection.cs:75-287 (.ValidateDataAnnotations().ValidateOnStart()) + Infrastructure/DependencyInjection.Caching.cs:93-101 (RedisDistributedLock-or-InProcessDistributedLock registration); Infrastructure/Concurrency/RedisDistributedLock.cs:36; API/Idempotency/IdempotencyFilter.cs:31-34,109,161-164 (IDistributedLock acquire; striped fallback :67-73,129); Tests/Presentation/MMCA.Common.API.Tests/Idempotency/IdempotencyFilterTests.cs:544 (4 DistributedLock_* facts); .github/workflows/ci.yml:725-767 (redis-integration); Infrastructure/Caching/DistributedCacheService.cs:189 (prefix-eviction warn); Shared/Resilience/HttpResilienceDefaults.cs:13,16,19,28; ADR-026; ADR-017 |
| 11 | Security | 3 | 4 | 8 | 12/24 | Authn centralized (JWKS/RS256+HS256 with algorithm pinning), authz at policy layer, secrets validated-not-committed, rate limiting + brute-force protection + security headers + CSP shipped, CI fails on vulnerable packages. Permission-based authorization: [HasPermission("x")] → on-demand perm:x policy via PermissionPolicyProvider → PermissionAuthorizationHandler checks the caller's roles/claims against an immutable IPermissionRegistry of role→permission grants, opt-in and compatible with the named role policies, 13 unit tests. The authenticated-only global rate limiter is ADR-documented (ADR-019). RS256 is pinned on both the in-process and the JWKS-forwarded validation paths (ValidAlgorithms = [RsaSha256]), closing cross-service alg-confusion. A framework IPasswordHasher (ADR-032) hashes with PBKDF2-HMAC-SHA512 (32-byte salt, 600k iterations, FixedTimeEquals compare) as the only supported algorithm, and the always-available brute-force LoginProtectionService is ADR-documented (ADR-029). A resource-ownership axis ships and is documented (ADR-033: OwnerOrAdminFilter 403s a single-resource route on a customer_id-claim mismatch, OwnershipHelper row-scopes collection queries, one admin bypass), opt-in per controller, claim-trusting, and ownership-not-ABAC. Opt-in vault-backed configuration ships via DefaultAzureCredential (Aspire/Configuration/KeyVaultConfigurationExtensions.cs:78,109), and SECURITY.md:83-86 documents it as AddCommonKeyVaultConfiguration (a no-op unless KeyVault:Uri is set) beside the sample's Key Vault URL plus managed-identity binding. The per-IP UI limiter and circuit ceiling (UI.Web/Hardening/UiRateLimitingExtensions.cs:145, BlazorCircuitLimitExtensions.cs:51) strengthen an already-met criterion. Held at Implementation 8 ("Strong", not "Exemplary"): the rubric-v2 threat-model criterion is wholly unmet (zero case-insensitive "threat model", "STRIDE" or "trust boundary" matches repo-wide; SECURITY.md at 95 lines carries no attack-surface analysis), authorization is RBAC with capability indirection plus opt-in ownership rather than a resource/attribute policy engine, and the failed-login counter is documented non-atomic (Infrastructure/Auth/LoginProtectionService.cs:55). Service-to-service trust is met only in part: the broker consumer restores user id, roles and tenant from unsigned MMCA-* headers (Infrastructure/Messaging/Consumers/ConsumerOriginRestore.cs:44,53, roles as plain text at Shared/Messaging/MessageHeaders.cs:31), and in-cluster gRPC forwards the user JWT over deliberate cleartext h2c (Grpc/DependencyInjection.cs:97-99, S5332 suppressed with a reason), with an opt-in app-layer service token (Grpc/Authentication/ServiceTokenServiceCollectionExtensions.cs:35,63,85,144, attached per client by ServiceTokenHttpClientBuilderExtensions.cs:28) that is HS256 over one key shared by every service (ServiceTokenSettings.cs:13,30), so any service can mint any service name, and that only Store adopts. Tenant resolution defaults to claim-only (Infrastructure/Persistence/Tenancy/TenancySettings.cs:61-62, header opt-in documented at :18-19; header read at API/Middleware/TenantResolutionMiddleware.cs:113), conformance-tested by the shipped TenantResolutionTestsBase (Source/Hosting/MMCA.Common.Testing/Conformance/TenantResolutionTestsBase.cs:27, subclassed at Tests/Hosting/MMCA.Common.Testing.Tests/Conformance/TenantResolutionConformanceTests.cs:11). The workflows carry no CodeQL or secret-scanning step. Evidence: Shared/Auth/IPermissionRegistry.cs + PermissionRegistry.cs:10 (FrozenDictionary/FrozenSet, case-insensitive roles); API/Authorization/{HasPermissionAttribute,PermissionPolicyProvider,PermissionAuthorizationHandler}.cs + AuthorizationExtensions.cs:38-42,68; API/Startup/WebApplicationBuilderExtensions.Authentication.cs:109 (forwarded-path ValidAlgorithms=[RsaSha256]), :238,254 (in-process ValidAlgorithms pinned per RS256/HS256), :196 (GetValidatedSigningKey 256-bit floor); API/Startup/WebApplicationBuilderExtensions.RateLimiting.cs:46 (IsRateLimitBypassed health/.well-known/grpc, ADR-019); .github/workflows/ci.yml:138 (fail-closed dependency audit); Tests/Architecture/MMCA.Common.Architecture.Tests/Api/AnonymousEndpointTests.cs:71 (explicit authorization decision on every controller and page); Infrastructure/Auth/LoginProtectionService.cs:50-56,74-82 (exp-backoff lockout + per-IP registration limit, ADR-029), :54-58 (lockout-backoff-overflow clamp); Infrastructure/Services/PasswordHasher.cs:9,53 (PBKDF2-only, FixedTimeEquals, ADR-032) + Tests/Core/MMCA.Common.Infrastructure.Tests/Services/PasswordHasherTests.cs (11 tests); API/Authorization/{OwnerOrAdminFilter,OwnershipHelper}.cs (resource-ownership axis, ADR-033); ADR-037 field-level-encryption converter ships but is latent/unadopted |
| 12 | Performance & Scalability | 2 | 4 | 8 | 8/16 | Async-throughout (no sync-over-async), projection/AsNoTracking/split-query/batch-by-IDs primitives + compiled-projection cache that prevent the §12 query red flags, stateless horizontal-scale design, a framework-level max-page-size clamp + unbounded-query ceiling (closing the "unbounded query" red flag), and a repeatable BenchmarkDotNet hot-path harness so hot-path efficiency is measured, not assumed. Maturity 4: the latency/allocation regression gate is a live merge gate. The Performance gate (BenchmarkDotNet Short + baseline verify) job runs the harness with --job Short --exporters json and a second step (build/perfgate) fails against the committed Tests/Performance/perf-baseline.json (deterministic per-benchmark allocation ceilings plus a machine-independent 1000x compiled-cache ratio floor); the job carries no continue-on-error, and its context is in the live required_status_checks list on main (8 contexts, all documented in CONTRIBUTING.md), so a red perf gate blocks the merge (ci.yml:310 job, build/perfgate run at :352, build/perfgate/Program.cs:61,84). The gate covers two benchmark files with eight allocation ceilings (perf-baseline.json:3-19, QueryPipelineBenchmarks.cs:17). Broker backpressure is configurable through the opt-in MessageBus:PrefetchCount and ConcurrentMessageLimit knobs (Infrastructure/Messaging/MessageBusSettings.cs:222,232, null by default so the transport default stands; applied only when set by ApplyBackpressure at Infrastructure/DependencyInjection.cs:1296 from the RabbitMQ :1233 and Azure Service Bus :1273 branches; the application half is untestable in the unit tier, DependencyInjectionBrokerMessagingTests.cs:174-176, so the two tests pin binding and null-default only). Held at Implementation 8, the top of Strong, because the criterion "load/stress tested against realistic volumes; capacity provisioning evidence-based" (ArchitectureEvaluationCriteria.md:389, no library carve-out) is met only in part: a load tier ships (Tests/Performance/MMCA.Common.LoadTests, OutboxThroughputLoadTests.cs and PagedQueryLoadTests.cs: 10k outbox messages, 100k-row paged queries, 50 concurrent reads), but .github/workflows/load-tests.yml:10,16 runs it weekly (cron 17 4 * * 0) or on dispatch only, on SQLite, and deliberately not as a required check (a load tier on every pull request is recorded as accepted, not adopted); the benchmarks cover only the specification and query-pipeline paths; and as a library Common carries no capacity-provisioning evidence. The lever to 9 is not yet identified. Evidence: .github/workflows/ci.yml:310 (performance-smoke, display name Performance gate (BenchmarkDotNet Short + baseline verify), matching the required context exactly) + .github/workflows/ci.yml:347-352 (build/perfgate baseline verify, no continue-on-error); Tests/Performance/perf-baseline.json:3-13 (allocation ceilings + ratio floor); build/perfgate/Program.cs (fails on ceiling/ratio violation); Tests/Performance/MMCA.Common.Benchmarks/SpecificationBenchmarks.cs:14 (spec compiled-cache + composition benchmarks); Application/Settings/ApplicationSettings.cs:15 (MaxPageSize=500) + API/Controllers/EntityControllerBase.cs:127 (clamp) + Application/Services/Query/EntityQueryPipeline.cs:100 (MaxUnboundedResultLimit); Infrastructure/Persistence/Repositories/EFReadRepository.cs:242 (AsNoTracking + split/single-query read primitives) |
| 13 | Observability & Operability | 2 | 4 | 8 | 8/16 | Full OTel (structured logs + RED metrics + distributed tracing) to OTLP and App Insights, liveness/readiness/dependency health checks driving orchestration, correlation IDs, deliberate poll-span noise filtering for cost. A warm-up readiness gate (ADR-025: WarmupReadinessHealthCheck, tagged ready) holds /health/ready unhealthy until startup warm-up has run, so the platform keeps traffic off a warming replica: wired unconditionally into AddServiceDefaults, free for every host, covered by 25 [Fact]/[Theory] across 4 files. Maturity 4: the readiness/health/OTel wiring is baked in, not opt-in, and unit-tested. The reference deployment declares SLO alerting as code: an actionGroup (samples/deployment/main.bicep:177), a sloAlertSpecs table (:196) expanded into four Microsoft.Insights/scheduledQueryRules resources (:237) wired to the action group (:280), paired with a runbook (samples/deployment/OPERATIONS.md) whose pairing is enforced by a CI-gated fitness test with a floor of 4 (Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/SampleDeploymentObservabilityTests.cs:12,21 over Testing.Architecture/Bases/Governance/ObservabilityConventionTestsBase.cs:30). EF query tags via QueryTagScope and the InternalCommands meter with its suppressed poll span enrich criteria already met. The sample also provisions an SLO workbook (samples/deployment/main.bicep:338, sloWorkbook of type Microsoft.Insights/workbooks), required by SampleDeploymentObservabilityTests.cs:24 (RequireWorkbook => true), which is also the shipped base's default (ObservabilityConventionTestsBase.cs:60). Held at Implementation 8 (Strong band, "nearly all criteria met, minor isolated gaps"): Common is a library, so alerts, dashboards and runbooks are provisioned and used only by deployers; the sample is compile-only in CI and the runbook set is the rubric section 29 restore drill plus OPERATIONS.md, the same deferred-to-consumer calibration that rows 17 and 29 carry. The lever to 9 is not yet identified. Evidence: Aspire/Extensions.cs:30-34 (ConfigureOpenTelemetry + AddDefaultHealthChecks + AddWarmupReadiness, all unconditional in AddServiceDefaults), Aspire/Extensions.Telemetry.cs:308-309 (Outbox/Cqrs meters), :122 (OutboxPollFilterProcessor), :100,129 (ProbeTelemetryFilter + its span processor), :166,174 (OTLP + Azure Monitor exporters), Aspire/Extensions.Health.cs:138 (liveness and readiness endpoints) + MMCA.Common.slnx:49 (the Aspire test project in the CI-gated tier); Aspire/Warmup/WarmupReadinessHealthCheck.cs:9 + Tests/Hosting/MMCA.Common.Aspire.Tests/Warmup/*Tests.cs (25 test attributes across 4 files); Application/UseCases/Decorators/LoggingQueryDecorator.cs:39 (query business-failures record outcome=failed, RED-metric parity with the command decorator) |
| 14 | Testability & Test Strategy | 3 | 4 | 9 | 12/27 | Right-shaped pyramid (~2,254 [Fact]/[Theory], weighted to Core, behind a --minimum-expected-tests 2000 zero-discovery floor on the unit tier, ci.yml:163); architecture fitness functions enforce dependency/purity/extraction/versioning/PII/specification/slice-cohesion/localization rules, authored once in the shipped MMCA.Common.Testing.Architecture package as 157 methods across 63 abstract bases (ADR-015) and re-run as thin subclasses (Common's own build executes 425, per FACTS.md:81,84); shared test infra is itself a shipped package, and a zero-discovery min-tests guard gates every merge. A translation-coverage fitness gate (LocalizationResourceTests) and a render-snapshot regression tier (PrimitivesSnapshotTests, 5 baselines, via the shipped MarkupSnapshot helper) sit beside permission-authz, fixed-TimeProvider and PiiRedactorTests coverage. CI enforces a 68.3% line-coverage floor that fails the build (unit tier, generated code excluded; measured ~70.3%). Maturity 4: CI-gated fitness tests + blocking coverage floor + zero-discovery min-tests guard. Held at 9 (not 10): there is no mutation testing on the Core tier (a repo-wide search for Stryker or any mutation-test configuration returns zero matches), and three further gaps are structural rather than polish: the integration tier is not a merge gate (Redis ci.yml:725, PostgreSQL ci.yml:769, SQL Server ci.yml:806, package-consumption and Bicep validation run on pull requests but sit outside the 8 required contexts), the module-wiring/AppHost tier (apphost-testing, ci.yml:846) fails the run but is not a required context, and messaging is never exercised against a real broker in this repo's CI. Evidence: Common's own Tests/Architecture/MMCA.Common.Architecture.Tests/{LayerDependency,DomainPurity,MicroserviceExtraction,DependencyVersion,PiiConvention,AggregateConvention,EventVersioningConvention,LocalizationResource,FrameworkSanity}Tests.cs + SpecificationFitnessTests.cs; Tests/Presentation/MMCA.Common.UI.Tests/Components/PrimitivesSnapshotTests.cs; Tests/Presentation/MMCA.Common.UI.Web.Tests/; .github/workflows/ci.yml:409 (68.3% coverage floor), :163 (the slnx test step carrying the --minimum-expected-tests 2000 guard) |
| 15 | Best Practices & Code Quality | 2 | 4 | 9 | 8/18 | Five analyzers at error + warnings-as-errors + lock files + NuGetAudit=all (with a blocking CI vulnerability-audit gate), consistent Result-based error handling, narrowly-scoped justified suppressions rather than blanket pragmas, ADRs capturing the why. The accepted-advisory list (NuGetAuditSuppress) carries five documented ImageSharp advisories (Directory.Build.props:48-52). Held at 9 (not 10): the repo-wide NoWarn carries RMG020;S8970;RS0041 plus two conditional blocks (CS1591 stays per project, FR-7), pragma disables are targeted and reasoned but accumulated, and one §15 code item is open, FR-2 (Result<T>.Value returns default on a failed result). Evidence: Directory.Build.props:7-13 (TWAE, lock files, NuGetAudit, audit=all, AnalysisMode=All), :106-122 (Meziantou/VS.Threading/Roslynator/Sonar/StyleCop package references), :30 (NoWarn RMG020;S8970;RS0041); .editorconfig:312 (analyzer default = error); .github/workflows/ci.yml:136 (Release build), :144 (blocking vulnerability audit via .github/actions/nuget-vulnerability-audit) |
| 16 | AI-Native Application Architecture | 2 | 4 | 9 | 8/18 | Scored because MMCA.Common.AI ships under ADR-120 and the rubric's applicability rule is met (ArchitectureEvaluationCriteria.md:473). Criteria walk-through (:476-482): five met, one partial, one unexercised. Model calls sit behind one port, provider SDKs in adapter packages chosen by a validated provider name, with both isolation directions gated (compile-time target + AiDependencyIsolationTests). Prompt and model versioning: the model id is required once enabled and hashed into the prompt contract behind a pin whose mismatch fails the build. Evaluation is gated in CI by a golden-replay base that refuses an empty corpus, over three recorded cases (the whole test step is skipped on a docs-only PR, ci.yml:153). Tool calling is least-privilege: AllowTools is false by default and tools are stripped entirely, every policy has to authorize, consequential tools need a per-request confirmation that never overrides a denial, and tools-on-with-no-policy throws at registration. LLM observability and cost: two token counters and a duration histogram tagged model, provider, prompt name, prompt version and outcome, recorded on success, error and cancellation alike, with prompt identity on the span when the caller carries the contract. Guardrails at the boundary (:479): ContentPolicyGuardrail matches eight built-in prompt-injection markers against user-role content only, redacted, refused or ignored per InjectionMode (Redact by default), plus any request pattern the host adds and a response-side refusal on configured blocked patterns; AddContentPolicyGuardrail binds Ai:ContentPolicy, validates every configured pattern at startup and registers one singleton under both IChatGuardrail and IChatRequestRedactor, pinned by fifteen behavior tests and six registration tests (the last a real host start that fails on an invalid pattern), inside the CI-gated solution; PiiRedactionGuardrail and per-update streamed inspection ship beside it. Retrieval (:481) is unexercised, not unmet: the criterion is worded conditionally on a vector or hybrid store existing, the bands count criteria met without defining an inapplicable criterion (:59-60), no embedding, vector or retrieval type exists in either AI project, and ADR-120 scopes it out; the ADC scorecard applies the same reading to the same sentence (adc-ArchitectureScorecard.md:68). None of the five red flags is present (:485-489). Held at Implementation 9 (not 10) by the one partial criterion, guardrails, on three residuals: both shipped policies are opt-in, so Ai:RequireGuardrail is satisfied by the PII redactor alone and a host can enable AI with no injection defense at all; the response half of the content policy is inert until a host configures patterns (the list is empty by default and the check short-circuits on an empty set); and the streamed path inspects fragments rather than the assembled answer, so a pattern spanning two updates is missed by design. Recorded, not deducted: the runaway-spend alert ships in the reference sample deployment the library cannot deploy itself (samples/deployment/main.bicep:227), type-checked by a Bicep job outside the eight required contexts, so the alert is available rather than deployed. Maturity 4: the prompt-contract pin, the golden-replay gate, the registration-time guardrail refusal and the startup pattern validation are all build-breaking. Evidence: Source/Core/MMCA.Common.AI/Guardrails/{ContentPolicyGuardrail.cs:64,115,143,164,176-189,269,294, ContentPolicySettings.cs:26,35,45,57, GuardrailServiceCollectionExtensions.cs:62,67-70,72-76} + Source/Core/MMCA.Common.AI/{AiSettings.cs:41-48,50-56,96,110, DependencyInjection.cs:139,186,208-217,219-225} + Chat/{BoundedChatClient.cs:190-192,249-265, UsageRecordingChatClient.cs:98,179, PromptTaggingChatClient.cs:54,58,63-68} + Observability/AiUsageMeter.cs:29,32,45,156-163; Source/Hosting/MMCA.Common.AI.Testing/{PromptContractPinTestsBase.cs:59-62, GoldenReplayTestsBase.cs:57-62}; Tests/Core/MMCA.Common.AI.Tests/{Guardrails/ContentPolicyGuardrailTests.cs:15,35,95, Guardrails/ContentPolicyRegistrationTests.cs:22,57,95, Evaluation/ReferenceGoldenReplayTests.cs:20-25}; Source/Build/MMCA.Common.LayerEnforcement.targets:86; Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/AiDependencyIsolationTests.cs:14; MMCA.Common.slnx:38; .github/workflows/ci.yml:87,153,163; samples/deployment/main.bicep:227; ADR-120 :40-47,388-391; ArchitectureEvaluationCriteria.md:59-60,473,476-482,485-489; adc-ArchitectureScorecard.md:68; FACTS.md:14 |
| 17 | DevOps & Deployment | 2 | 3 | 8 | 6/16 | CI gates build+analyzers+tests+vuln-audit+coverage-floor on every pull request (the workflow is pull_request-only) and release automates pack/SBOM/publish on tags. Publishing is dual-registry and keyless (ADR-053): every release pushes to both GitHub Packages and nuget.org, the nuget.org leg exchanging an OIDC token for a short-lived single-use key via NuGet/login with no stored API key, under a least-privilege permissions block, with a blocking CycloneDX SBOM gate, and the publish jobs sit behind a reviewer-gated release environment. An in-repo reference deployment sample ships: samples/deployment/{foundation,main}.bicep (Container Apps + ACR-via-managed-identity + Key Vault + SQL + cost tags + budget) + DEPLOYMENT.md (OIDC federated-credential setup, UAMI bootstrap, post-deploy smoke-gate/auto-rollback), with the secrets array the secretRef: 'sql-conn' binding needs declared (samples/deployment/main.bicep:151-152, consuming binding at :164). A sample-deployment-validate job compile-checks both templates with az bicep build on every pull request. Maturity 3 (not 4): the validate job is a compile check only, the workflow's own comment states a real what-if/deploy stays a consumer-side concern, and the job's context is absent from the 8 required status checks on main (read live from the branch-protection API), so it is not a merge gate, let alone automatic enforcement of the CD/IaC-apply axis. Implementation 8 (not 9): validation is not deployment execution; the sample is a reference, not a self-deploy (a library cannot deploy itself, DEPLOYMENT.md:3), the package-consumption job also sits outside the required contexts, a release has no rollback beyond the irreversible nuget.org versions, and full CD machinery lives in consumer repos. A scheduled Azure smoke-deploy of the sample is recorded as accepted, not adopted. Evidence: samples/deployment/; .github/workflows/ci.yml:14-16 (pull_request-only triggers), :703-705 (the "a real what-if/deploy stays a consumer-side concern" comment), :707 (sample-deployment-validate, display name "Validate samples/deployment Bicep") with its two compile-only az bicep build steps at :719 and :723, absent from the 8 required contexts; .github/workflows/release.yml:20-23 (least-privilege + id-token: write), :107 (blocking CycloneDX SBOM generate-or-fail step), :133 (NuGet/login keyless nuget.org trusted publishing, ADR-053), :19,156 (environment: release, reviewer-gated per the live environments API); DEPLOYMENT.md:3 (reference sample, explicitly not self-deploying); newest §17-tagged entry CHANGELOG.md:4047 |
| 18 | UI Architecture & Components | 3 | 4 | 9 | 12/27 | Clean container/presentational split: reusable smart DataGridListPageBase<TDto> + dumb parameterized primitives composed via RenderFragment slots + EventCallback, shared across both consumer apps, gated by bUnit + a gallery render-smoke job. No 1000-line blobs / logic in markup. Evidence: MMCA.Common.UI/Pages/Common/DataGridListPageBase.cs:18; Components/{EmptyState,PageHeader,PageLoadingState,PageErrorState,DeleteConfirmation}.razor; Components/MobileInfiniteScrollList.razor:19,49-52 (typed [Parameter,EditorRequired], EventCallback, @key) |
| 19 | State Management & Data Flow | 3 | 4 | 9 | 12/27 | One explicit owner per state, all per-circuit scoped (no static mutable cross-user state), unidirectional flow via params/EventCallbacks, StateHasChanged marshalled via InvokeAsync, reliable current-dirty reads via IsDirtyAccessor (the wrapped-guard stale-IsDirty red flag), and a disposed-CTS-safe cancellation swap (swap-fresh-CTS-before-dispose + catch (ObjectDisposedException)). Fresh state sits behind an explicit client-side staleness policy: IUiReadCache/UiReadCache with a config-bound UiReadCacheOptions policy (Enabled=true by default, 60s default TTL, longest-prefix per-route TTLs), registered unconditionally by AddCommonUI, read-through on all four EntityServiceBase read methods (successes only, explicit bypassCache), prefix invalidation on every successful write, and Clear() on both sign-out paths; NotificationState.IsStale/MarkStale gates the bell's per-navigation refetch behind a configured NavigationRefreshMaxAge. 27 unit tests (15 + 12) live in the CI-gated MMCA.Common.UI.Tests project. Maturity 4: the guard, disposed-CTS and cache/staleness behaviors are all unit/bUnit-tested inside the CI-gated .slnx. Held at 9 (not 10): the cache is an in-memory per-circuit layer (no cross-tab or offline story). Evidence: MMCA.Common.UI/Services/Caching/IUiReadCache.cs:32,51,59,66 (Get/Set, InvalidatePrefix, Clear); MMCA.Common.UI/DependencyInjection.cs:70-79 (TryAddScoped scoped-state registrations); Services/Notifications/NotificationState.cs:8 (per-circuit scoped state, IsStale/MarkStale); Components/UnsavedChangesGuard.razor:34,36,52 (IsDirtyAccessor/CurrentIsDirty); DataGridListPageBase.cs:553-570 (disposed-CTS swap-before-dispose); Tests/Presentation/MMCA.Common.UI.Tests/Components/UnsavedChangesGuardTests.cs:17 |
| 20 | Design System & UI Consistency | 2 | 4 | 8 | 8/16 | MudBlazor is the only component library (the RCL ships no Bootstrap, wwwroot/app.css:108), over a centralized theme/token system (palette, spacing scale and z-index scale tokenized), light and dark variants, and a CI-enforced drift guard binding five CSS tokens plus the Secondary family to the C# source of truth (Theme/BrandColors.cs:10, Tests/Presentation/MMCA.Common.UI.Tests/Theme/BrandColorTokenTests.cs:34,60), with a BrandColorTokenTestsBase shipped for consumers; library quirks are fixed once in shared wrappers (grid paging guardrail, Pages/Common/DataGridListPageBase.cs:63). The dark palette is user-toggleable (ADR-028): MudThemeProvider @bind-IsDarkMode driven by a ThemeService (cookie + localStorage + User.PreferredTheme, OS-default), with a ThemeToggle (Theme/ThemeToggle.razor:7) in the shared MainLayout beside the culture switcher (ADR-028 notes the no-flash SSR theme bootstrap is not wired). Both palettes meet WCAG AA contrast (dark PrimaryContrastText/ErrorContrastText = rgba(0,0,0,0.87)), locked by a blocking dark-mode axe gate (Tests/Presentation/MMCA.Common.UI.E2E.Tests/Layout/DarkModeE2ETests.cs:30) that runs on all three required ui-e2e engines (ci.yml:232) beside the light-mode AA gate. Maturity 4: the token drift guard and the axe gates are automatic, UIArchitectureConventionTests subclasses the shared base, and theming is documented (ADR-028, the dark-mode contrast record at Website/docs-src/guides/common-ACCESSIBILITY.md:51). Common's razor files carry 0 inline style declarations, enforced by InlineStyleTests (Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/ComponentConventions/InlineStyleTests.cs:12) over the shared InlineStyleTestsBase, and the layout restates no raw brand hex (Layout/ReconnectModal.razor.css:100 reads var(--mmca-primary)). Held at Implementation 8 (not 9) because the encapsulated-overrides criterion is not at reference quality: the shared layout scoped CSS fights MudBlazor with 42 !important declarations (28 in Layout/NavMenu.razor.css, with raw nav colors instead of tokens; 14 in Layout/MainLayout.razor.css, whose badge override block at :76 duplicates NavMenu.razor.css:274-292), kept deliberately because they beat MudBlazor's own styles (removing them is recorded as accepted, not adopted); and the brand hex is still defined twice, in C# and CSS (Theme/BrandColors.cs:13 and wwwroot/app.css:63), mirrored by the drift gate (BrandColorTokenTests.cs:35-38) rather than sourced from one token. The two !important lines left in app.css carry comments naming the MudBlazor specificity they beat. The lever to 9 is not yet identified. Evidence: Theme/MMCATheme.cs:66,93 (dark PrimaryContrastText/ErrorContrastText), :56 (dark palette); Theme/BrandColors.cs:10-19 + BrandColorTokenTests.cs:34; Layout/DarkModeE2ETests.cs:30 + .github/workflows/ci.yml:232 (dark-mode axe gate on three required engines); Theme/ThemeToggle.razor + MainLayout.razor (ADR-028) |
| 21 | Accessibility (a11y) | 3 | 4 | 9 | 12/27 | Semantic landmarks, real keyboard-operable controls, ARIA names, stated WCAG 2.1 AA target enforced by a required chromium axe gate in CI covering the loading (named loading indicator) and error (alert) component states + shipped E2E a11y bases + a documented manual screen-reader pass (ACCESSIBILITY.md: landmarks/focus-order/ARIA-names/form-error association). PageLoadingState renders role="status" with a named spinner (WCAG 4.1.2). Both themes are AA-clean: dark PrimaryContrastText/ErrorContrastText = rgba(0,0,0,0.87), and the dark-mode axe scan is a blocking gate (DarkModeE2ETests in the required chromium ui-e2e job). Maturity 4 on the required chromium axe merge gate. Held at 9 (not 10): no automated focus-trap/reading-order assertion (manual pass only), and axe breadth is scoped to the gallery's representative states (deep consumer states are scored downstream). Evidence: Layout/MainLayout.razor:22,33,88 (skip-nav + role="main" + role="alert"); Components/PageLoadingState.razor:3 (role=status + named loading indicator); Theme/MMCATheme.cs:60,73 (dark contrast); Tests/Presentation/MMCA.Common.UI.E2E.Tests/DarkModeE2ETests.cs:30 (dark-mode WCAG 2.1 AA axe gate); common-ACCESSIBILITY.md (light+dark AA pass + known limitations: manual-only focus-trap/reading-order; canonical in the Website repo under docs-src/guides/); .github/workflows/ci.yml:223,232 (ui-e2e job and its three-engine matrix, all required); Testing.E2E/Infrastructure/AxeOptions.cs:22 (WCAG 2.1 AA tag set); Tests/Presentation/MMCA.Common.UI.E2E.Tests/ComponentsPageE2ETests.cs:82 (loading/error-state axe coverage) |
| 22 | Responsive & Cross-Browser | 2 | 4 | 9 | 8/18 | Fluid MudBlazor breakpoints, explicit grid→card mobile alternative, density options on DataGridListPageBase (a DenseGrid property + ToggleDensity() round-tripping through ListPageState: URL key d, in-memory, sessionStorage; unit-tested in ListPageStateServiceTests/ListPageQueryStateServiceTests), the shared .mmca-touch-target 48px affordance (cart drawer + mobile cards + data-grid pager, enforced by a phone-viewport Playwright bounding-box test), and a documented RESPONSIVE.md device/breakpoint matrix. A 3-engine cross-browser matrix runs in CI and all three engines block merges: the chromium, firefox and webkit ui-e2e contexts are all in required_status_checks (read live from the branch-protection API), with no continue-on-error in the job. Held at 9 (not 10): the matrix runs against the backend-less gallery's representative states, and RESPONSIVE.md's device rows are a documented matrix, not device-lab execution. Evidence: .github/workflows/ci.yml:232 (3-engine matrix of the ui-e2e job at :223, all required merge gates; no continue-on-error in the job); MMCA.Common.UI/Pages/Common/DataGridListPageBase.cs (DenseGrid/ToggleDensity) + Services/ListPageStateService.cs/ListPageQueryStateService.cs (DenseGrid persisted); wwwroot/app.css:199 (.mmca-touch-target shared rule); RESPONSIVE.md (breakpoint + browser matrix); Tests/Presentation/MMCA.Common.UI.E2E.Tests/ComponentsPageE2ETests.cs (touch-target viewport test); Common/BreakpointConstants.cs:16 (<960px mobile) |
| 23 | Front-End Performance | 2 | 4 | 8 | 8/16 | Server-side paging (no load-everything-then-page-in-memory), cancellation of superseded fetches, SSR persist/restore to kill a redundant round-trip, bounded infinite-scroll DOM (MaxRenderedItems cap), @key-stable rendering, skeleton/loading states, and opt-in MudDataGrid row virtualization (DataGridListPageBase.cs:155,643 with a VirtualizeServerData funnel, adopted by the gallery /grid page over 1,000 rows and proven in-browser by E2E facts asserting the rendered window stays under 200 rows, GridPageE2ETests.cs:26,43). Maturity 4: WebVitalsE2ETests asserts Core Web Vitals budgets (LCP 2500 ms, FCP 1800 ms, TTFB 800 ms, CLS 0.1, INP 200 ms, Tests/Presentation/MMCA.Common.UI.E2E.Tests/WebVitals/WebVitalsE2ETests.cs:19-23) on the gallery Login, Components and Grid pages (:58, via the shipped WebVitalsCollector) and a sampled INP interaction, asserted per metric (WebVitalsCollector.cs:143-151) inside the required ui-e2e merge gate on chromium, firefox and webkit (ci.yml:223-292); the budgets were tightened on measured evidence (WebVitalsE2ETests.cs:43-46). Asset hygiene is clean: wwwroot is about 398 KB with favicon.png (1,148 bytes) the only image, fonts are latin-subset woff2, and the csproj carries a Content Remove for wwwroot\**\*.map. Held at Implementation 8, the top of Strong: virtualization is opt-in and the budget gate measures only the gallery; there is no bundle-size or payload budget on any merge gate (the composite action .github/actions/wasm-payload-budget/action.yml ships in Common and both consumers run it on pull requests, ADC at 5,900 KB and Store at 5,700 KB, but in neither is it a required check; Common has no WASM app of its own, so running it in Common's ci.yml does not apply); route-level WASM lazy loading is evaluated and not adopted (see the backlog); no component in Common UI overrides ShouldRender; and the only caching-header code sets no-store on HTML (UI/Extensions/WebApplicationExtensions.cs:34), with no static-asset caching policy. Response compression is registered in the API pipeline (API/Startup/WebApplicationBuilderExtensions.cs:70) and admin search debounces at 300 ms over server-side paging (Pages/Administration/UserAdminList.razor:23). Evidence: DataGridListPageBase.cs:155,643 (VirtualizeGrid opt-in + LoadVirtualizedServerDataAsync); Tests/Presentation/MMCA.Common.UI.Gallery/Pages/GridGallery.razor:44 (gallery adoption); Tests/Presentation/MMCA.Common.UI.E2E.Tests/Layout/GridPageE2ETests.cs:26,43 (bounded-window proof) + WebVitals/WebVitalsE2ETests.cs:58,66,74,92 (CWV budget on Login, Components, /grid and the INP interaction); .github/workflows/ci.yml:232 (3-engine ui-e2e matrix, required contexts); DataGridListPageBase.cs:542 (ServerData + ResetCancellationTokenAsync), :186,198 (PersistentComponentState); MobileInfiniteScrollList.razor.cs:52,277 (MaxRenderedItems cap, IntersectionObserver), MobileInfiniteScrollList.razor:30 (@key); consumer payload budgets MMCA.ADC/.github/workflows/deploy.yml:476-497, MMCA.Store/.github/workflows/deploy.yml:493-514 (PR-only, not required) |
| 24 | Forms, Validation & UX Safety | 2 | 4 | 8 | 8/16 | Unsaved-changes guard with reliable current-dirty reads, submit/loading/error/empty states, destructive-action confirmation, client/server password-rule parity and field-level error association: Register/Login/ForgotPassword/ResetPassword are EditForm + per-field ValidationMessage over typed models, with a shared form-level ErrorSummary for form-level/server errors, a ModelValidation bridge onto MudForm adopted by NotificationSend with its length caps read from the shared request contract (Pages/Notifications/NotificationSend.razor:48-70), and DeleteConfirmation for destructive actions. Maturity 4: the auth-form, guard and sessions behavior is pinned by bUnit tests in MMCA.Common.UI.Tests (MMCA.Common.slnx:48) and the client/server parity Theory runs in MMCA.Common.Architecture.Tests (MMCA.Common.slnx:59), both in the unit tier run by the required build-and-test job (ci.yml:163). The shared FormsConventionTestsBase is consumer-scoped (it matches Source/Modules/**/*Create.razor string markers, Testing.Architecture/Bases/Ui/FormsConventionTestsBase.cs:43, and in Common only its Profile half runs, against ChangePasswordCard, Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/FormsConventionTestsBaseTests.cs:42), so the in-repo enforcement rests on these tests. Implementation 8: five criteria are met and the parity criterion is met on the auth pages but not on the shared change-password card. (1) Parity: on Register and ResetPassword, client and server evaluate one shared Unicode-aware rule (Shared/Auth/PasswordComplexity.cs:52), to which Pages/Auth/PasswordComplexityAttribute.cs:24 and the server's StrongPasswordRules (Application/Validation/CommonValidationRules.cs:191, 128-character cap at :197) both delegate, pinned by a Theory over non-ASCII rows (Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/PasswordRuleParityTests.cs:36). The shared ChangePasswordCard breaks that parity: its client check tests only Length >= MinLength (Components/Auth/ChangePasswordCard.razor.cs:88-91) and leaves MaxLength unbounded by default (:45), while the server applies StrongPasswordRules to change-password (MMCA.ADC/Source/Modules/Identity/MMCA.ADC.Identity.Application/Users/Validation/ChangePasswordRequestValidator.cs:18), so a password such as "abcdefgh" passes the form, is refused by the server, and the refusal surfaces as a toast (ChangePasswordCard.razor.cs:130) rather than a field message, the rubric's "client validation that disagrees with the server" red flag (ArchitectureEvaluationCriteria.md:673) in one isolated place, with the server still authoritative. (2) Localized errors: the auth models carry resource keys (Pages/Auth/RegisterModel.cs:16, ResetPasswordModel.cs:17-30) resolved by the localizing validator (Register.razor:33, Login.razor:40), with Spanish siblings (Resources/SharedResource.es.resx:707) pinned by RegisterFormTests.cs:71. (3) Destructive actions: the per-row revoke and "Sign out everywhere" both confirm first (Pages/Auth/Sessions.razor.cs:130, :195), tested on the ask and cancel paths (SessionsTests.cs:335). (4) Dirty tracking: the shipped UnsavedChangesGuard is wired on both in-repo pages that hold editable state, each to a real dirty signal (Pages/Administration/RoleAdminEdit.razor:7, Pages/Notifications/NotificationSend.razor:25). (5) States: disabled + spinner + aria-busy on every submit (Login.razor:67-68), with no double-submit path. (6) Accessible validation: role="alert" summaries, For= association and fieldset/legend grouping (Components/Forms/ErrorSummary.razor:12,25-30). ResetPasswordModel.NewPassword carries [StringLength(128, ...)] (ResetPasswordModel.cs:27-28), matching the server's MaximumLength(128) (CommonValidationRules.cs:197). Held at 8 (not 9) by the change-password parity gap above, beside smaller ones: server FluentValidation messages are English literals (Application/Auth/Validation/LoginRequestValidator.cs:16, CommonValidationRules.cs:191-201), Register has no unsaved-changes guard, the shipped forms gate matches string markers, and forgiving input (masks, undo) is thin. The lever to 9 is to have ValidateNewPassword call the shared PasswordComplexity rule and default MaxLength to the server cap, so the card rejects what the server rejects with a field-level message (deferred to the next Common release). Evidence: Pages/Auth/{Register,Login,ForgotPassword,ResetPassword}.razor + {RegisterModel,LoginModel,ForgotPasswordModel,ResetPasswordModel,PasswordComplexityAttribute}.cs; Shared/Auth/PasswordComplexity.cs; Tests/.../Pages/Auth/{AuthModelValidationTests,RegisterFormTests,SessionsTests}.cs plus the ModelValidation, ErrorSummary, UnsavedChangesGuard and DeleteConfirmation component tests in the CI-gated MMCA.Common.UI.Tests; Components/Forms/{UnsavedChangesGuard,ErrorSummary,DeleteConfirmation}.razor (UnsavedChangesGuard.razor:17,36,52, live accessor pre-empting the one-render lag) |
| 25 | Navigation & Information Arch | 2 | 4 | 9 | 8/18 | Route-level auth via AuthorizeRouteView (not just UI hiding), role/claim-filtered navigation, focus management on navigate, query-state preservation, and both 404 and 403 handled in the app shell: a dedicated Forbidden (403) page renders for the authenticated-but-unauthorized branch, alongside NotFound. A NavigationFlow.md documents the Common UI surface's routes + role/claim model (mirroring ADC's). Maturity 4: the navigation-contract drift gate ships and gates merges: NavigationContractTests reflects over every routable MMCA.Common.UI page (RouteAttribute/AuthorizeAttribute) and asserts route set-equality plus auth-posture agreement against the embedded NavigationFlow.md routes table, with a non-vacuous 8-route floor, in the CI-gated unit tier, which closes the §25 red flag (navigation drifting from the documented per-role design). Parameters are typed and validated: a typed, constrained deep-link route ships (@page "/notifications/inbox/{Id:int}", Pages/Notifications/NotificationInbox.razor:2) with the :int constraint documented as the validation boundary (a malformed id renders NotFound, NavigationFlow.md:21). Held at 9 (not 10): one typed route exercises the criterion; broader deep-link breadth is consumer-side. Evidence: Pages/Notifications/NotificationInbox.razor:2 (typed constrained route); NavigationFlow.md:21 (validation-boundary documentation); Tests/Architecture/MMCA.Common.Architecture.Tests/NavigationContractTests.cs:29,44 (set-equality + auth-posture drift gate) + MMCA.Common.Architecture.Tests.csproj:11 (NavigationFlow.md embedded resource); MMCA.Common.slnx:56 (gate in the CI-gated unit tier); Pages/Forbidden.razor + Routes.razor:11 (AuthorizeRouteView: NotAuthorized→<Forbidden/>, NotFoundPage); Components/RedirectToLogin.razor (ReturnUrlProtector); Tests/.../Pages/ForbiddenTests.cs |
| 26 | Front-End Security | 3 | 4 | 9 | 12/27 | No unsanitized HTML/eval (grep MarkupString|innerHTML|eval over MMCA.Common.UI = zero), centralized hardened security-headers middleware with a pluggable ICspPolicyProvider CSP extension point (ADR-023, replacing per-host hand-rolled headers), unit-tested open-redirect protection, explicit "client untrusted / server re-validates" posture, no client-shipped secrets, and access tokens held in-memory with the refresh token in an HttpOnly cookie (no localStorage; the comments in ITokenStorageService state tokens are NOT stored there). The SSR token-storage scheme (HttpOnly access/refresh cookies seeded at login, an SSR-time non-validating read handler for [Authorize] on fresh GETs, refresh-never-leaves-the-server) is documented in ADR-022 (a BFF-style design whose security argument rests on the API remaining the validation boundary). SecurityHeadersSettings.ContentSecurityPolicy defaults to the complete hardened baseline default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none', string-pinned by a unit test in the CI-gated .slnx, and a per-request {nonce} facility ships alongside it (placeholder substitution + CspNonce.Get, tested in enforce and report-only modes). Maturity 4: the hardened baseline, HSTS and nonce behaviors are unit-tested. Held at 9 (not 10): style-src retains 'unsafe-inline' for the MudBlazor inline-style reality (SecurityHeaders.cs:68), the nonce path is opt-in for hosts, in the default non-proxy mode the refresh token passes through JS at login (mmca-auth-cookie.js:10-16), and no fitness test or analyzer bans a future MarkupString or innerHTML. Evidence: Aspire/Security/SecurityHeaders.cs:67-69 (complete default CSP value with both directives; directive-strength rationale at :53-66), :214-217 (HSTS emission), :86-93 (ICspPolicyProvider extension point and its static default), :124-139 (CspNonce.Get per-request nonce facility) + :268-290 (AddCommonSecurityHeaders, TryAddSingleton default, first-registered provider wins, at :288); Tests/Hosting/MMCA.Common.Aspire.Tests/Security/SecurityHeadersMiddlewareTests.cs:157-161 (DefaultSettings_ContentSecurityPolicy_IsHardenedBaseline, the hardened-baseline pin), :177-194 (enforce-mode {nonce} substitution), :218-227 (report-only {nonce} mode), ADR-023; ITokenStorageService.cs:5-6; Services/Navigation/ReturnUrlProtector.cs:18-60 + tests |
| 27 | Internationalization (i18n) | 1 | 4 | 8 | 4/8 | Multi-locale i18n (en-US + Spanish, ADR-027 Decision 9) ships in the framework itself and default-on: co-located .resx + IStringLocalizer<T>, backend errors localized server-side at the edge keyed by the existing Error.Code (English Message as fallback), one culture cookie as the source of truth across SSR/Server/WASM (forwarded to services as Accept-Language), and a User.PreferredCulture persisted per user with a CultureSwitcher in the shared layout. Maturity 4 (Optimized) because it is machine-enforced: the translation-coverage gate fails the build on a missing or incomplete es sibling, with a non-vacuous floor (Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/LocalizationResourceTests.cs:21); the literal gate (LocalizedTextConventionTests) fails the build on hard-coded snackbar/title/<PageTitle>/breadcrumb/NavItem literals; MA0076 culture-less formatting is a build error (.editorconfig:592); the plural-sentence convention check scans every Source .resx with a non-vacuous floor and detector self-tests (Ui/PluralSentenceResourceTests.cs:18,30,41-67); and the pseudo-localization pass runs in the required ui-e2e merge gate (PseudoLocalizationE2ETests renders five paths under qps-Ploc, PseudoLocalizationE2ETests.cs:31, asserting that the [!! sentinel appears on each page, :57, plus no horizontal overflow under ~40% expansion and an en-US leak guard). SharedResource is a complete 223/223 en/es pair, MudBlazor chrome localizes via ResxMudLocalizer over MudTranslations.{resx,es.resx} (145/145), NavItem.TitleResource makes module nav culture-aware, the auth models carry resource keys (Pages/Auth/RegisterModel.cs:16) resolved by Validation/LocalizedDataAnnotationsValidator.cs:54, and pluralization runs through IStringLocalizer.Plural(key, count, args) with .One/.Other selection (Globalization/StringLocalizerPluralExtensions.cs:40,46,51, used at Pages/Notifications/NotificationSend.razor.cs:117), with the Spanish singular values accented like their siblings (Resources/SharedResource.es.resx:464,467,470). UTC instants render in the viewer's time zone through ViewerTimeZone (Services/Culture/ViewerTimeZone.cs:18, registered at MMCA.Common.UI/DependencyInjection.cs:198, used at Pages/Notifications/NotificationInbox.razor:58, NotificationList.razor:82 and Pages/Auth/Sessions.razor.cs:244); the server's emitted error codes are covered for es (API/Resources/ErrorResources.resx and ErrorResources.es.resx hold 81 entries each, Auth.InvalidCredentials at :115 in both) and pinned by an emitted-code coverage gate (Tests/Presentation/MMCA.Common.API.Tests/Localization/ErrorResourceCoverageTests.cs:158); and the default transactional emails localize subject and body to the recipient's PreferredCulture with a pluralized minutes count (Users/UseCases/ForgotPassword/ForgotPasswordHandlerBase.cs:117,136, Users/Emails/UserEmailResources.cs:32, SendEmailConfirmationHandlerBase.cs:156). Held at Implementation 8 (Strong band, isolated gaps, ArchitectureEvaluationCriteria.md:59) on breadth: there is no RTL support evidence, and the lever to 9 is not yet identified. The breadth caveats: two locales with zero RTL under Source/Presentation, a two-category plural mechanism with the CLDR selector deferred (StringLocalizerPluralExtensions.cs:14-18), and the culture list hard-coded in the framework (Shared/Globalization/SupportedCultures.cs:18). Evidence: Tests/Presentation/MMCA.Common.UI.E2E.Tests/PseudoLocalizationE2ETests.cs; Tests/Architecture/MMCA.Common.Architecture.Tests/{LocalizedTextConventionTests,LocalizationResourceTests}.cs; Source/Hosting/MMCA.Common.Testing.Architecture/{ArchitectureRules.LocalizedText.cs,Bases/LocalizedTextConventionTestsBase.cs}; Source/Presentation/MMCA.Common.UI/Globalization/ResxMudLocalizer.cs + Resources/MudTranslations.{resx,es.resx} + DependencyInjection.cs (TryAddTransient + Pseudo decorator); Pages/Common/ErrorMessages.cs ([Obsolete] Success, sanitized templates); Common/NavItem.cs (TitleResource); Tests/Presentation/MMCA.Common.UI.Gallery/GalleryHost.cs (test-only qps-Ploc allowlist); ADRs/027-multi-locale-i18n.md sections 8 and 9 |
| 28 | Front-End Testing & Quality | 3 | 4 | 9 | 12/27 | Right-shaped pyramid (many bUnit component + service unit tests, fewer Playwright E2E), a shipped shared E2E package reused across apps, axe WCAG 2.1 AA + render-smoke as a real-browser CI merge gate against a self-hosted gallery, robust role/test-id selectors with auto-wait (no sleep-flakiness), per-test trace artifacts. A render-snapshot (golden-markup) regression tier (MarkupSnapshot, shipped in Testing.UI for consumer reuse) normalizes per-render MudBlazor GUIDs and diffs the markup of shared primitives against committed baselines, failing the build on an unintended structural change; it is deterministic and OS-independent (markup, not pixels), so it runs in the in-solution unit tier on every platform with no per-platform golden management. Held at 9 (not 10): it is a markup-snapshot, not pixel visual-regression, and E2E journeys center on Identity/auth. Evidence: Source/Hosting/MMCA.Common.Testing.UI/Infrastructure/MarkupSnapshot.cs; Tests/Presentation/MMCA.Common.UI.Tests/Components/PrimitivesSnapshotTests.cs + Snapshots/*.html; Testing.E2E/** (PageObjects, abstract *TestsBase); Infrastructure/PageExtensions.cs:113-132,185-210 (FillAndVerify auto-wait + axe); .github/workflows/ci.yml:223,232 (ui-e2e gate, all three engines required) |
| 29 | Resilience & Business Continuity | 3 | 4 | 8 | 12/24 | Failure-isolation is strong and fitness-enforced (timeouts/retries/circuit breaker on every outbound client + outbox graceful degradation), the chaos/fault-injection test trips a breaker and asserts BrokenCircuit short-circuiting, the warm-up/readiness gate is unit-tested (gate latch/idempotency/thread-safety, opens even on task failure), and OpenIdConnectMetadataWarmupTask pre-warms OIDC to kill ACA cold-start. Maturity 4: the in-repo restore drill (DatabaseRestoreDrillTests: seed → backup → catastrophic wipe → restore → verify on ephemeral SQLite) runs on every CI build in the unit tier (a build gate, not a scheduled cron, which is the distinction that keeps a non-gating drill at M3), and the resilience guide (common-RESILIENCE.md) records the framework's measured restore baseline (~5 ms median RTO over 5 runs, 0-row RPO byte-for-byte asserted) alongside the SLO/error-budget template, so the recovery procedure is demonstrated, measured and automatically enforced in-repo. Held at Implementation 8: the framework's own guide records production RTO/RPO, restores and SLO alerting as the consumer's (Website/docs-src/guides/common-RESILIENCE.md:5,31), with the SLO section an unfilled template (:95); production RTO/RPO against real cloud backups and measured production SLOs are consumer IaC. Evidence: Tests/Core/MMCA.Common.Infrastructure.Tests/Resilience/DatabaseRestoreDrillTests.cs:27 (data-loss recovery + measured-RTO facts under a 30 s ceiling; project in MMCA.Common.slnx:39); common-RESILIENCE.md:46 (measured baseline + SLO template); Aspire/Warmup/ + Tests/Hosting/MMCA.Common.Aspire.Tests/Warmup/*Tests.cs; Tests/.../ResilienceCircuitBreakerFaultInjectionTests.cs:17-61 (ADR-009/025) |
| 30 | Compliance, Privacy & Governance | 2 | 3 | 8 | 6/16 | Answers the headline §30 red flag (soft-delete-as-only-deletion-path) with an explicit erasure extension point, AES-256-GCM encryption, bounded retention, and a build-gating PII fitness function, and answers the second §30 red flag ("PII in logs/telemetry") with the shipped, tested PiiRedactor, which masks every [Pii]-marked member (shallow, value-erasing) before an entity carrying personal data is written to a structured log or telemetry attribute. A PiiErasureContractFitnessTests build gate forces a [Pii]-marked DataSubjectSample through PiiRedactor + IAnonymizable end-to-end. DSAR and audit-trail substance ships: a DataExportControllerBase DSAR export surface behind the Privacy.DataExport feature flag (ADR-076, marked permanent, CHANGELOG.md:456), a field-level audit trail whose interceptor records the redaction token for [Pii] members on both sides of every change (AuditTrailSaveChangesInterceptor.cs:309-310, CI-gated by a unit test) with a 90-day retention purge job (ADR-075), and a shared erasure workflow that soft-deletes then irreversibly anonymizes in place (Source/Core/MMCA.Common.Application/Users/UseCases/DeleteUser/DeleteUserHandlerBase.cs:133). Maturity 3 (not 4): the surfaces are opt-in twice over (AuditTrailSettings.Enabled defaults false, the purge runs only if the host also runs the scheduler; the export is off until a host enables the flag), and the privacy gates are the PiiConventionTests marking rule (non-vacuous, Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/PiiConventionTests.cs:10-14) and PiiErasureContractFitnessTests, a mechanism test over one in-test type; DataResidencyTestsBase has only its self-test probe in-repo (Governance/DataResidencyTestsBaseTests.cs:24); and the governing process (consent capture, residency verification) is consumer-resident, so the category as a whole is not automatically enforced (the rubric's M4 bar). PRIVACY.md ships in-repo and the CSV export is fail-closed (API/Controllers/EntityControllerBase.cs:231,272). Implementation 8: consent and lawful basis are absent from the framework (zero matches in Domain, Application and Infrastructure; PRIVACY.md:20 states the framework records neither), the lawful-basis inventory gate PiiInventoryTestsBase (Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Privacy/PiiInventoryTestsBase.cs:33) has only a self-test subclass in-repo (Governance/Privacy/PiiInventoryTestsBaseTests.cs:14), and residency verification is an app concern, the same "deeper substance deferred" calibration as row 11. Evidence: ADR-005; Domain/Privacy/PiiRedactor.cs:24-142 ([REDACTED] token, per-type reflection cache); Tests/Core/MMCA.Common.Domain.Tests/Privacy/PiiRedactorTests.cs (7 [Fact]s, incl. "never emits the clear-text PII values"); Tests/Architecture/.../PiiErasureContractFitnessTests.cs:19-40 (forces a [Pii] type through redactor + anonymize); Domain/Interfaces/IAnonymizable.cs:22-31 (idempotent Result-based Anonymize); Infrastructure/Persistence/Encryption/EncryptedStringConverter.cs:31,34 (AES-256-GCM, 12B nonce/16B tag, ADR-037-documented; shipped but unadopted/latent, no production column encrypted) |
| 31 | Cost Efficiency / FinOps | 2 | 3 | 8 | 6/16 | Real design-level cost awareness on the telemetry/poll-interval axis (OutboxPollFilterProcessor + idle-vCPU socket tuning + a released cost guide), with in-framework execution levers rather than docs alone: a built-in head-based OTel trace sampler knob (Telemetry:TracesSampleRatio → ParentBasedSampler(TraceIdRatioBasedSampler), unit-tested, the largest trace-ingestion lever) and the outbox per-message "dispatched" log at Debug (caps the highest-volume log line). Health-probe traffic does not bill: the default-ON Telemetry:FilterProbeTelemetry knob wires ProbeTelemetryFilter into both the ASP.NET Core and HttpClient tracing instrumentation (Aspire/Extensions.Telemetry.cs:100-109, config key const at :19), and a ProbeTelemetryFilterProcessor registered before the exporters un-records the probe-child dependency spans the inbound filter cannot reach, the health check's SQL SELECT 1 and the Redis PING (Extensions.Telemetry.cs:129, Aspire/Telemetry/ProbeTelemetryFilterProcessor.cs:20); metrics are deliberately untouched, so probe traffic stays on dashboards (the "Cost release", CHANGELOG.md:2380). Telemetry:DisableHttpClientMetrics and DisableRuntimeMetrics hold under UseAzureMonitor() through MetricStreamConfiguration.Drop views (CHANGELOG.md:2425), and Telemetry:EnablePollyDurationMetrics is a fifth rubric §31-annotated cost knob built to the same shape, covered by 7 tests (CHANGELOG.md:1569). Maturity 3: one cost convention is enforced automatically. The shipped CostTagConventionTestsBase fails the build when a consumer's infra/main.bicep declares a container app or per-service database without a per-service cost-attribution tag (Testing.Architecture/Bases/Governance/CostTagConventionTestsBase.cs:6-12), self-tested in-repo (Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Deployment/CostTagConventionTestsBaseTests.cs:11) and subclassed by ADC and Store in their CI-gated architecture tiers (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Governance/CostTagConventionTests.cs:12). M4 is not met: nothing enforces cost at review time (the pull-request checklist carries no cost, budget or telemetry-volume item, .github/pull_request_template.md:22-29, and CONTRIBUTING.md has zero cost matches), and the gate's doc comment mislabels itself rubric section 34. Held at Implementation 8 (not 9): right-sizing, reversible scale-ups and spend attribution are consumer-side actions the framework only recommends (Website/docs-src/guides/common-COST.md:65,71,74), and cost attribution plus budgets exist only as compile-only sample IaC. Evidence: Aspire/Extensions.Telemetry.cs:100,129 + Aspire/Telemetry/ProbeTelemetryFilterProcessor.cs:20 + Tests/Hosting/MMCA.Common.Aspire.Tests/Telemetry/*Tests.cs (27 [Fact]/[Theory] across ProbeTelemetryFilterTests 12, ProbeTelemetryFilterProcessorTests 11, ProbeTelemetryToggleTests 4; project at MMCA.Common.slnx:46); Aspire/Extensions.Telemetry.cs:138-139 (TracesSampleRatio sampler) + Tests/.../Telemetry/TracesSampleRatioTests.cs; Infrastructure/.../Outbox/OutboxProcessor.cs (LogMessageProcessed=Debug); Aspire/Telemetry/OutboxPollFilterProcessor.cs:34-45; common-COST.md:69 (right-sizing framed as a deployment choice); samples/deployment/main.bicep:353-354 (conditional budget, costCenter tag at :51, compile-only in CI) |
| 32 | Dependency & Supply-Chain | 3 | 4 | 9 | 12/27 | Hits essentially every §32 criterion for a published framework: central pinned versions, 26 lock files + audit=all gated in CI across feeds, source-mapped trusted feed, SemVer via MinVer, CycloneDX SBOM a hard release gate, signed SLSA build-provenance attestation on every packed .nupkg, Apache-2.0 license (patent grant; committed LICENSE file), and (uniquely) a fitness function (ADR-016) turning the licensing-driven MassTransit-v8 pin into a build gate so the "blanket update reintroduces known-bad dependency" red flag cannot recur. A committed .github/dependabot.yml (weekly NuGet + Actions PRs) carries version-update:semver-major ignores for every held package (MassTransit, VS.Threading.Analyzers, StackExchange.Redis, MessagePack), so the pin discipline lives in config as well as in the test, plus a periodic sweep to latest stable. ADR-038 (supply-chain provenance) records the layered posture in one decision record. Held at 9 (not 10): license-compliance tracking, a named criterion, has no CI artifact, and the coordinated consumer sweep is policy, not automated. Evidence: Directory.Packages.props:54-56 (MassTransit 8.5.5 pinned + v9 commercial-license-crash note :63); Directory.Build.props:8-10; Tests/Architecture/.../DependencyVersionTests.cs + base DependencyVersionTestsBase.cs:24-37 (fails if MassTransit major≥9); .github/dependabot.yml:24-35 (held-package semver-major ignores); release.yml:107,225 (CycloneDX SBOM via the shared ./.github/actions/cyclonedx-sbom composite action, blocking); release.yml:98,217 (signed SLSA build-provenance attestation on every packed .nupkg); 26 packages.lock.json; ADR-016; ADR-038 |
| 33 | Developer Experience & Inner Loop | 2 | 4 | 8 | 8/16 | Strong inner loop: a documented, gitignored cross-repo source override (the rubric's explicit local.props example: copying local.props.template sets UseLocalMMCA, CONTRIBUTING.md:117, ignored at .gitignore:423), one-obvious-command build, test, single-test, arch-test, pack and FACTS commands (AGENTS.md:12), analyzer and editorconfig parity local-to-CI, and a getting-started guide that installs the template with one command, lists its prerequisites and needs no credentials (Website/docs-src/guides/common-GETTING-STARTED.md:14, six numbered steps), with MMCA.Helpdesk as a worked, runnable companion. Maturity 4: the consumer-source-build canary builds MMCA.Helpdesk against the pull request's framework source via UseLocalMMCA and runs its tests behind a floor of 40 (.github/workflows/ci.yml:422,527), carries no continue-on-error (the workflow has none: even the AppHost tier is blocking, ci.yml:846, :851-852), and its Consumer source build (Helpdesk) context is one of the eight required checks (CONTRIBUTING.md:67, matching the live branch-protection API), so a framework change that breaks a source-mode consumer blocks the merge automatically. Implementation 8 (not 9): the criteria are met with several small, documented gaps: source mode binds a consumer to Common's last-built Debug reference assembly, so a cross-repo edit needs a manual Debug rebuild (CONTRIBUTING.md:150); contributors are told to expect CI-only analyzer and restore round-trips, because a green source-mode build is not proof CI is green (CONTRIBUTING.md:167); the Aspire AppHost stalls when launched headless and must run from an interactive terminal (common-GETTING-STARTED.md:154-155), and the AppHost test tier blocks the run but is not a required merge check (ci.yml:851-852), nor is package-consumption; and the agent guardrail hooks live outside this repo, whose only tracked Claude config is an allow-list with stale entries (.claude/settings.local.json:1). CI and contributors run the same package-consumer check script. The UseLocalMMCA package swap list is generated from a glob over Source/**/MMCA.Common.*.csproj plus the metapackage (build/LocalSource/MMCA.Common.LocalSource.targets:32) and imported by each consumer's Directory.Build.targets:6. The lever to 9 is not yet identified. Evidence: .github/workflows/ci.yml:422-628 (required cross-repo canary); CONTRIBUTING.md:67 (required-checks list incl. Consumer source build (Helpdesk)), :117 (local source mode), :150, :167; AGENTS.md:12; the published getting-started guide; build/LocalSource/MMCA.Common.LocalSource.targets:32 |
| 34 | Architecture Governance & Docs | 2 | 4 | 8 | 8/16 | Reference-quality governance: one hundred thirty-two ADRs (001-132, canonical in ../adr/, count and range owned by ../adr/README.md:6, which also carries the index and the "Writing a new ADR" guide), each non-obvious decision matched to a fitness function; architecture rules enforced automatically twice over (compile-time MSBuild target + a shared NetArchTest rule library, ADR-015) plus dependency-version/PII/aggregate/event-version/specification/slice-cohesion fitness functions; a published getting-started guide (Website/docs-src/guides/common-GETTING-STARTED.md, canonical in the Website repo) + current CLAUDE.md. FACTS.md is generated from source by a dependency-free build/facts tool and CI drift-gated (a --check step fails the build when the committed facts drift, deliberately not code-guarded so it runs on docs-only PRs), so version (v1.244.0, FACTS.md:4,14), package count (22, FACTS.md:19) and fitness counts (157 methods across 63 bases, Common executing 425, FACTS.md:81,84) are a computed-and-gated artifact rather than hand-maintained prose; the rubric (ArchitectureEvaluationCriteria.md) and FACTS.md are version-controlled. An ADR template ships at Website/docs-src/adr/_template.md, and the workspace-level architecture map that the published ADR index points deep mechanics at (Docs/Architecture/ArchitecturalAnalysis.md, docs-src/adr/README.md:154) marks its body figures historical and points at FACTS.md for the current ones (ArchitecturalAnalysis.md:3). Held at 8 ("Strong", not "Exemplary"): some ADR index rows have grown into long revision logs, and Common carries no dedicated change-coupling or tech-debt-ledger gate of its own; the lever to 9 is not yet identified. Evidence: ADRs 001-132 (Accepted/dated; ../adr/README.md:6 index + "Writing a new ADR" guide); build/facts/FactsGenerator.cs + .github/workflows/ci.yml:118-121 (FACTS generated + --check drift gate) + FACTS.md; ArchitectureEvaluationCriteria.md (rubric in-repo); Source/Build/MMCA.Common.LayerEnforcement.targets AND Tests/Architecture/.../*Tests.cs (subclassing Source/Hosting/MMCA.Common.Testing.Architecture); Website/docs-src/guides/common-GETTING-STARTED.md |
Weighted column = Maturity·weight / Implementation·weight per row.
Scoring notes
- N/A (excluded from denominators): none. All 34 rows carry numeric Maturity and Implementation, so Σweight is 82. §16 AI-Native Application Architecture is scored because
MMCA.Common.AIships a model-calling feature and the rubric's applicability rule is met (ArchitectureEvaluationCriteria.md:473); §27 Internationalization is scored because ADR-027 supersedes the single-locale ADR-011. - §32 weight = 3 (raised from the default 2 because MMCA.Common is a published framework).
- Rubric version 2 (ADR-110): §10 is Messaging & Integration Architecture at weight 3, and §16 is AI-Native Application Architecture at weight 2.
- Implementation 10 is awardable for an almost perfect implementation (every criterion at reference quality, no red flags, at most trivial polish left), so the index reads directly against 100%. The denominators are ×4 and ×10. The backlog's scheduling target is 9: the 9→10 rung is recognition at re-score time, not scheduled work, so the implementation band ranks only categories at Implementation 8 or below.
- Axis-gap calibration: §17 is mature-but-execution-deferred (the mechanism ships in-repo, deployment proof lives downstream); §12, §13 and §29 hold Maturity 4 with Implementation 8 because load testing, dashboards, production SLOs and real-backup RTO/RPO are deployer-owned; §30 and §31 are the inverse (in-repo implementation leads a maturity capped by consumer-owned governance and cost process). Together these are the structural source of the gap between the two indices.
- A FLAG never shrinks the denominators: a category whose proposed move is refuted on the adversarial pass is carried at its adjudicated score.
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).