Skip to content

refactor(arch): move Access into Modules/Access - #1095

Merged
mforce merged 4 commits into
mainfrom
refactor/1087-access-module
Oct 6, 2026
Merged

mforce merged 4 commits into
mainfrom
refactor/1087-access-module

Conversation

@mforce

@mforce mforce commented Oct 5, 2026 •

Copy link
Copy Markdown
Owner

Refs #1087

S8 of the package-by-module design: Access moves into Modules/Access in Application, Infrastructure and Api. This is a pure move with no behaviour change. Its repository and EF configurations stay in place until S9, and UserRoleAssignment stays in Farm. Proofs compare this head ad0aac70 with the base deae5245 (#1094 merged). After #1094 merged, I rebuilt the branch on it: I re-ran the codemod for split and move, which re-derived the tenant-bypass rows, and cherry-picked the hand-written fix commit unchanged.

One deviation from the design's inventory, approved by the owner

The owner approved this split on 2026-10-05, for two reasons: a move PR must not change ownership, and each public type gets its own file.

The inventory moves Application/Common/IIdentityProvider.cs and IStepUpGrantService.cs whole. But today the Types claim gives Access only the two interfaces. The other 7 types in those files are Platform: FarmSignIn, RefreshTokenRevocationOutcome, TokenPair, UserSummary, UserProfile, StepUpGrant and StepUpErrorCodes. A whole-file move makes them Access-owned. I measured that change, and it alters the dependency policy:

  • UserProfile names Domain.Catalog.EggUnit, so it adds an Access → Commerce edge symbol. The analyzer fails the build with CW1001.
  • AuthEndpoints.LegacyUpgradeResult would reach Access through TokenPair, a non-contract type. AdapterReachRealTreeTests reports that as a contract bypass.

So the first commit splits those 7 types into their own files in Application/Common, each a verbatim slice. Only the two interfaces move. Ownership stays exactly as on main: the same types are Access, Platform and contract.

Review order

  1. split (mechanical): the 7 Platform types leave the two port files for their own files in Application/Common, in the same namespace.
  2. move (generated; skim it): 82 git mvs, with namespaces taken from folders. tools/module-move/move.py rewrote references, usings, ledger strings, tenant-bypass Symbol/File strings and paths. prune then dropped the usings the move left unused.
    • The rules file's Namespaces now holds Cluckwork.Application.Modules.Access and Cluckwork.Infrastructure.Modules.Access.
    • Its Types claim is deleted: the two ports now sit in Application/Modules/Access/Users/, so the namespace gives them to Access.
    • Implementations is unchanged until S9.
  3. fix:
    • AccessOwnershipClaimTests now resolves each port through the ledger's namespace index. It requires a namespace claim (not a type claim) owned by the contracted Access module.
    • GlobalUserLookupGuardTests and AccountSuspensionTests build Identity paths from segments, which the path rewrite does not match.
    • The simulation runbook and bootstrap.sh pointed at a stale Identity/SimulationOptions.cs; it lives in Persistence.
    • The k6/config.js comment now gives the new AuthCookies.cs path.
  4. marks: deletes the 29 Access [ModuleContract] marks.

FarmDirectoryCallerTests needed no change: no Access file names IFarmDirectory or AccountRepository.

Change map

Before After
Application/Features/Users/** contracts (21 files, 29 types) Application/Modules/Access/Contracts/
Application/Features/Users/** handlers, validators, IUserRoleAssignmentRepository, PasswordRules, UserName Application/Modules/Access/Users/**
Application/Common/{IIdentityProvider,IStepUpGrantService}.cs (interfaces only, after the split) Application/Modules/Access/Users/
Infrastructure/Identity/*.cs (30 files) Infrastructure/Modules/Access/Identity/
Api/Endpoints/{Auth,Me,Users}/*.cs (6 files) Api/Modules/Access/{Auth,Me,Users}/: Platform-owned adapters
BypassAllowList.cs (9 rows), FilterFreeSetSites.cs (32 rows) Symbol/File strings follow the namespace. No Hash or Signature line changed

Checks at the head (base measured on deae5245)

Check Base Head
dotnet build Cluckwork.sln (analyzers on) 0 warnings 0 warnings
Application.Tests Architecture + TenantBypass + Documentation + ImagePin 474/474 474/474
Application.Tests Users/Access/Identity/Password/StepUp/Auth classes 22/22 22/22
Domain.Tests 495/495 495/495
Integration: ServingGuardCoverage, TrackedMutationRead, InsightsReadOnly, GlobalUserLookupGuard, AccountSuspension, RenameAccountDocs, SecretCliLoggingGuard 31/31 31/31

CI at ad0aac70

18 checks pass, including the four test legs, "Build, audit and schema docs", both image smoke tests and the three Playwright shards. "Publish the commit image" is skipped, as on every PR.

Tenant-bypass registry, before and after (#632 keys)

The rows were parsed from BypassAllowList.cs and FilterFreeSetSites.cs at the base and at the head. Base rows were then mapped through the move's type and path table.

Check Result
Row counts allow-list 40 → 40, filter-free 36 → 36
Every row after the map, all fields identical in both files
Hash (allow-list) and Signature (filter-free) multisets identical: no hash changed
Re-keyed rows 41: 9 allow-list (Symbol, File), 32 filter-free (Symbol). No other field differs
Live scanner vs registry TenantBypassRealTreeTests green at the head. Each hash is SHA-256 over the member's Roslyn tokens, so unchanged hashes mean no query token changed in any of the 41 members

Proofs (base deae5245 vs head ad0aac70)

Proof Result
EF migrations has-pending-model-changes "No changes have been made to the model since the last migration." No migration added. The only Migrations/ diff is 11 snapshot lines naming ApplicationUser, ApplicationRole and RefreshToken under their new namespace. Control: ApplicationUser.Language at HasMaxLength(17) makes the same command exit 1
tools/schema-docs/generate.sh --check "docs/schema/ is up to date"
Ordered DI descriptors: Production/Development × serving/one-shot × Redis absent/configured (8 dumps) identical after the type map
Generated OpenAPI document (Development) identical
Exact normalized contract set per owner identical: 160 types; Access 29 (all in Application Contracts)
Module-edge report (live edges) identical after the type map and owner-root renames
Peer-reach report identical
Table-owner report identical
Adapter-reach report identical (Access has no module registration file, so no new adapter entry)
coupling-matrix.md unchanged (CouplingMatrixRealTreeTests green)
Unused usings (IDE0005, whole solution) base 214, head 201, 0 new. The 13 removed were already-unused usings of the emptied Infrastructure.Identity, Application.Features.Users and Api.Endpoints.Auth namespaces

Mutations

Each case ran in a spare worktree at the head. It was reset, built, and run against the Architecture + TenantBypass filter. When the build itself failed, the case was rebuilt with analyzers off and warnings not treated as errors, so the guards still ran.

# Mutation Result
M1a codemod moves Contracts/DisableUserCommand.cs (file and namespace) back to Users/DisableUser/ red AdapterReachRealTreeTests, CouplingMatrixRealTreeTests; contract set Access −DisableUserCommand
M1b same file moved, .Contracts namespace kept red NamespaceFolderAgreementTests only
M2 UserEndpoints also injects IUserRoleAssignmentRepository red AdapterReachRealTreeTests, CouplingMatrixRealTreeTests
M3 moved PemKey references Finance's Expense (no Access → Finance edge) build red CW1001. With analyzers off: red ModuleLedgerRealTreeTests, CouplingMatrixRealTreeTests (2)
M4 peer ConfirmSaleHandler also takes IUserRoleAssignmentRepository red PeerContractRealTreeTests
M5 contract AccessUserSummary gains an Account aggregate property red ModuleContractRealAssemblyTests; also CW1001 Access → Farm from a contract type, ModuleLedgerRealTreeTests, CouplingMatrixRealTreeTests
M6 codemod moves IIdentityProvider back to Application/Common red AccessOwnershipClaimTests. Also CW1002 stale edge symbol, ModuleLedgerRealTreeTests, CouplingMatrixRealTreeTests
M7 allow-listed CredentialEpochVerifier.VerifyAsync: account.Id == user.AccountId → account.Id == accountId red TenantBypassRealTreeTests.RealSourceTree_AllBypassesAreAllowListed
M7c control: same member, only a /* probe */ comment added inside the query green: comments are trivia, so M7's red comes from the token
M8 filter-free AccessSeedLookup.OwnerRoleExistsAsync: r.Name == Roles.Owner → r.Name != Roles.Owner red TenantBypassRealTreeTests.RealSourceTree_FilterFreeSetSitesAreStableAndClassified

mforce added 4 commits October 5, 2026 23:40
…med ports

move.py split: IIdentityProvider.cs keeps only IIdentityProvider, and
IStepUpGrantService.cs only IStepUpGrantService. FarmSignIn,
RefreshTokenRevocationOutcome, TokenPair, UserSummary, UserProfile,
StepUpGrant and StepUpErrorCodes each go to their own file in
Application/Common, the same namespace, as verbatim slices. Access's Types
claim covers only the two interfaces, so these seven are Platform today;
moving them with the interfaces would make them Access-owned, adding an
Access -> Commerce edge symbol (UserProfile names EggUnit) and an adapter
reach through a non-contract type (AuthEndpoints through TokenPair).

Refs #1087
move.py move with the slice's table: the Users feature (Application), the
Identity code (Infrastructure) and the Auth, Me and Users endpoints (Api)
go under Modules/Access, contract types under Contracts, with the
namespace taken from the folder. IIdentityProvider and IStepUpGrantService
leave Application/Common for Application/Modules/Access/Users, so the
rules file drops its Types claim: their namespace now gives them to
Access. The rules file names Cluckwork.Application.Modules.Access and
Cluckwork.Infrastructure.Modules.Access; Implementations stay for S9.
Tenant-bypass symbols and files, edge symbols and the guards' path lists
follow by name. move.py prune drops the usings the move left unused.

Refs #1087
AccessOwnershipClaimTests asserted the two ports through Access's Types
claim, which the move retires. It now resolves each type through the
ledger's namespace index and requires a namespace claim owned by the
contracted Access module. GlobalUserLookupGuardTests' fixture and
AccountSuspensionTests' source read build their Identity paths by
segments, which the codemod's path rewrite does not match. Two comments
and the simulation runbook pointed at a stale
Identity/SimulationOptions.cs; it lives in Persistence.

Refs #1087
The twenty-nine Access contract types now sit in Application's Contracts
folder; the folder makes them contract, and a type with both fails.

Refs #1087
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant