Skip to content

refactor(access): declare fixture contracts and preserve actors (#857 E2) - #1060

Merged
mforce merged 6 commits into
mainfrom
chore/857e2-access-contract
Oct 4, 2026
Merged

mforce merged 6 commits into
mainfrom
chore/857e2-access-contract

Conversation

@mforce

@mforce mforce commented Oct 4, 2026 •

Copy link
Copy Markdown
Owner

Access now declares its final 28-type contract. Demo and simulation reconstruct real actors through a fixture-only read port; simulation creates cast users through Access operations. The two Common identity ports become Access-owned, closing the deferred #1023 bypass.

This is E2, following #1055 (E1). Review the new ports and ledger first, then the two small seeder diffs. Session/profile minting remains the A–D implementation; the requested deeper session reshape needs its own design.

Change map

Before E2 (E1):
  Demo -> UserManager<ApplicationUser> + IAccountUserDirectory
  Simulation -> those reads + IIdentityProvider + IUserRoleAssignmentRepository
  CurrentUserContext + SimulationOptions -> Identity namespace (Access)
  Common identity/step-up interfaces -> Platform ownership; no Access contract

After E2:
  Demo + Simulation -> IAccessSeedLookup -> directory + UserManager + scoped context
  Simulation -> IAccessOperations.CreateUserAsync -> IIdentityProvider (one call)
  Simulation -> scoped assignment read/add -> Owner audit -> one save [#858 P5]
  CurrentUserContext + SimulationOptions -> Persistence (Platform), same bodies
  Lifecycle verbs -> isolated lifecycle graph; real guard poisons UserManager/Identity/Redis
  Effective-role lookup -> one public method with the original query body
  ICurrentUser -> the same scoped CurrentUserContext instance
  Common identity/step-up interfaces -> Access types claims, outside the contract
  Access -> 28 contract entries + UserRoleAssignmentRepository implementation

The contract contains seven ports, nine values and twelve commands. IUserRoleAssignmentRepository is absent because it exposes an aggregate. Platform→Access adapter reach falls from A48 to A28; no peer W/R edge is added or widened. The generated matrix is regenerated with CLUCKWORK_REGENERATE_MATRIX=1. The 20 stale simulation→Access adapter ceilings, including deleted RolesOfAsync, are pruned; the final report has zero Loosenable entries. Five Access compatibility rows remain for #858: three AspNetRoles prerequisites, the AspNetUsers total count and the assignment write. The repository's one filtered Flocks LEFT JOIN is re-dated to #859. The two seeders, persistence registration and moved options/context are left ready for #858 P5; no fixture configuration key changes.

Properties to refute

  • A peer or adapter can take either Common identity port, a generic-wrapped Access repository, or an aggregate-bearing contract value without a guard failing. Both ownership claims are tested independently, with their respective claim-removal green controls.
  • Demo's real DI/read path constructs IdentityProvider or Redis, a lifecycle verb constructs UserManager/Identity/Redis, or the fixture read port is registered in Production.
  • An account/email collision, alternate normalizer, disabled Owner or multiple roles changes the selected actor. Each profile chooses the lowest-Id active Owner and uses every actual role; missing/failed rereads resolve no actor.
  • Fixture creation changes the Worker null role, password, password-change flag, cancellation or forwarding count. Reruns create no duplicate user, assignment or audit.
  • Simulation certifies polluted/partial data, changes worker rotation or attribution, drops the real flock name, or commits the assignment before its audit. The assignment uses the original ordered untracked read, factory and single save.
  • The actor alias loses identity or scope isolation, authorization reads become cached/fail-open, confirm reads move ahead of the order lock, or target admission runs before proof.

GetAssignedFlocksAsync documents that its caller must pass the resolved actor's id or a verified tenant member; zero visible assignments also applies to an unknown/foreign id. Null is unrestricted and read failures propagate. The #1023 walker still has its documented method-body, return/property/field and Platform-helper limits. IAccessOperations is registered in Production and bypasses interactive step-up; only review restricts it to operator CLI/simulation callers. IAccessSeedLookup accepts explicit cross-farm account ids outside Production; only review restricts its callers to the two seeders. No new caller guard is added.

Validation

  • Full Application suite: 722/722, zero skips; solution build: zero warnings/errors on main 20198660 plus E2. Measured baseline at 7014566c and E1: 704/704; E2 before test(arch): match build symbols and reject inactive source #1056: 706/706.
  • Targeted integration: 374/374 across 49 initial classes, plus 87/87 in nine final fix-round classes, all run serially. The restored lifecycle guard is 7/7; lookup is 6/6; query warnings are 3/3. Seeder parity: 54/54 on baseline and 54/54 on E2 across 16 classes, including every sibling in the two requested fixture files.
  • 66/66 mutations fail named assertions. The exact reviewer UserManager factory probe went 7/7 green before the guard fix, then failed all six real-verb assertions with Expected: 0, Actual: 1; the restored guard passes 7/7. The pruned ceiling has its own before-green/after-red probe. Identity/Redis/cache and role-predicate mutants were refreshed after inlining. All 16 architecture mutations and both independent claim-removal green controls were repeated after integrating test(arch): match build symbols and reject inactive source #1056. The main merge changed no E2 runtime source; the subsequent fix only inlines the unchanged effective-role query body.
  • Real-tree measurement: 1069 peer members, 402 adapters / 139 non-empty reaches. Contract, ownership, seam, table-owner, compatibility, query-shape and generated-matrix guards pass.

Baseline/head seeder snapshots match: demo has exactly 977 audit rows (Customer.Create 3, DailyEntry.Create 481, DailyEntry.Submit 480, Flock.BirdMovement 1, Flock.Create 3, Product.Create 3, SalesOrder.AddItem 3, SalesOrder.Confirm 1, SalesOrder.Create 2), all signed by its Owner. Simulation has exactly 453 audit rows with the same complete action/actor groups and worker/submitter rotation, and six products.

The fixed-clock simulation manifest matches completely: 2 accounts; 10 users (1 Owner, 1 Manager, 1 Sales, 3 Workers, 4 ReadOnly); 101 customers; 102 flocks (100 active/1 depleted/1 archived); 55 bird movements; 24 entries (4 draft/10 submitted/10 locked); 100 egg lots; 6 orders (2 draft/4 confirmed); 1 payment; 2 inventory items/lots; 122 inventory movements (2 purchase/8 usage/111 adjustment/1 discard); 8 feed and 8 water rows; 2 expense categories/5 expenses. Anchor, completion and fingerprint match. Cross-day/partial/changed-definition siblings pass independently too.

Full seeder SQL differs only by the explicit account-scoped Owner reread at statement six: demo 4389→4390 and simulation 1772→1773. Only parameter names are normalized. The 42 B SQL/HTTP artifacts remain identical; the D verb/purge artifacts match after only timestamp/elapsed normalization. Context/options bodies match main after only namespace replacement.

The #1023 amendment records that #1033 plus E2 completes its acceptance criteria. The #858 handoff records 37 remaining rows (29 simulation, 8 demo), including the five Access rows and P5's assignment operation. Both issue bodies retain their original history.

Quality and deslop record

Ran deslop, pstack:thermo-nuclear-code-quality-review, ponytail-review and pstack:make-pr-easy-to-review myself. The read implementation is 36 lines and reuses the existing directory/normalizer. Simulation shrinks 2644→2641 lines; demo grows 427→429; no new source file reaches 1000 lines. Removed the redundant roles-copy helper, single-caller effective-role forwarder and implementation dependencies. Both rekeyed TSV sites retain query token 4adcdb84 and their rationale. Kept the existing compact single-table ledger arrays and ordinary Common import. No cache, lazy factory, service location or fallback actor is added. Ponytail: Lean already. Ship.

Opus E1 S1 is addressed by the assignment membership precondition comment. Lifecycle dependency guards now poison UserManager as well as IdentityProvider/Redis. The exact Astra factory probe is assertion-red; no lifecycle constructor dependency was added. The optional E1 helper-inline suggestion is implemented with the query body, predicates and cancellation unchanged.

E2 changes SimulationOptionsBindingTests only by its namespace import. The approved review round adds UserManager poison to AccessLifecycleDependencyTests and renames its theory; all seven cases and their assertions stay intact. E1's four separately approved mechanical edits were disclosed in #1055: decorator forwarder deletion in MustChangePasswordGateTests and StepUpAuthTests, Users imports in AccountProvisioningTests and TwoFarmIsolationMatrixTests. None of their assertions changed.

Local evidence: /home/mforce/.cluckwork-slices/reviews/857-E2-evidence/; layered quality record: /home/mforce/.cluckwork-slices/reviews/857-E2-quality/. The initial tests-first commit is e245a8b. Astra approved 3731894a; Opus found no blocking security defect at that head. The owner-requested combined fix addresses Q1, the UserManager guard, the role forwarder and the documented caller boundaries. S3/Q2/Q3 behavior remains unchanged; the final delta remains at the coordinator’s security hold point.

66 independently restored assertion-red mutations

Every row ran its named test and failed an assertion. Compiler/environment failures are excluded. The table quotes an excerpt of the recorded failure; full messages and TRX are retained locally.

Mutation Named assertion Failing assertion (excerpt)
access-contract-erased AccessOwnershipClaimTests.CommonIdentityPortRemainsOwnedByContractedAccess(type: "Cluckwork.Application.Common.IStepUpGrantService") “Assert.NotEmpty() Failure: Collection was empty”
actor-cancellation-dropped AccessSeedLookupTests.SeedReadsPropagateCancellation “Assert.ThrowsAny() Failure: No exception was thrown; Expected: typeof(System.OperationCanceledException)”
actor-loses-account-predicate AccessSeedLookupTests.ActorReadsPreserveAccountDisabledStateAndEveryActualRole “Assert.Null() Failure: Value is not null; Expected: null; Actual: AccessActor { Id = d872932d-2654-4bca-943c-34c8a4b1e872, Email = shared-actor-35b5fe6586124dff8461855f2bd0e291@test.local, DisabledAt = 10/04/2026 04:09:52 +00:00, Roles = <>z__ReadOnlyArray`1[System.String] }”
actor-loses-disabled-state AccessSeedLookupTests.ActorReadsPreserveAccountDisabledStateAndEveryActualRole “Assert.NotNull() Failure: Value of type 'Nullable<DateTimeOffset>' does not have a value”
actor-truncates-actual-roles AccessSeedLookupTests.ActorReadsPreserveAccountDisabledStateAndEveryActualRole “Assert.Equal() Failure: Collections differ; ↓ (pos 1); Expected: ["Admin", "Manager", "ReadOnly"]”
aggregate-dto-property ModuleContractRealAssemblyTests.RealLedger_EveryContractTypeIsFreeOfPersistenceAndAggregates “module contract guard failed:; Cluckwork.Application.Features.Users.AccessUserSummary.op_Inequality exposes Cluckwork.Domain.Accounts.UserRoleAssignment via AccessUserSummary -> AccessUserSummary.Assignment; Cluckwork.Application.Features.Users.AccessUserSummary.op_Equality exposes Cluckwork.Domain.Accounts.UserRoleAssignment via AccessUserSummary -> AccessUserSummary.Assignment”
aggregate-on-contract ModuleContractRealAssemblyTests.RealLedger_EveryContractTypeIsFreeOfPersistenceAndAggregates “module contract guard failed:; Cluckwork.Application.Features.Users.IUserRoleAssignmentRepository.ListByUserAsync exposes Cluckwork.Domain.Accounts.UserRoleAssignment via Task<IReadOnlyList<UserRoleAssignment>> -> IReadOnlyList<UserRoleAssignment> -> UserRoleAssignment; Cluckwork.Application.Features.Users.IUserRoleAssignmentRepository.ListAllAsync exposes Cluckwork.Domain.Accounts.UserRoleAssignment via Task<IReadOnlyList<UserRoleAssignment>> -> IReadOnlyList<UserRoleAssignment> -> UserRoleAssignment”
assignment-before-order-lock ConfirmSaleOrderBoundaryTests.ConfirmReadsRoleAndAssignmentsAfterTheOrderLock(disableWorker: True) “Assert.Equal() Failure: Collections differ; ↓ (pos 1); Expected: ["account lock", "order lock", "active actor"]”
assignment-flock-name-corrupt SimulationSeederTests.SimulationSeed_RestrictedWorkerAssignment_PreservesActorAndAuditDetails “Assert.Equal() Failure: Strings differ; Expected: "Sim House A"; Actual: "Sim House Awrong"”
assignment-implementation-removed CompatibilityExceptionRealTreeTests.RealSourceTree_EveryCompatibilityExceptionIsRegistered “compatibility exception guard failed:; undeclared compatibility exception Cluckwork.Infrastructure.Repositories.UserRoleAssignmentRepository.AddAsync -> Access at src/Cluckwork.Infrastructure/Repositories/UserRoleAssignmentRepository.cs:56. Read through the module's contract, or add this row to compatibilityExceptions and fill in owner, reason and deleteWhen:; { "symbol": "Cluckwork.Infrastructure.Repositories.UserRoleAssignmentRepository.AddAsync", "reaches": "Access", "tables": ["UserRoleAssignments"], "owner": "", "reason": "", "deleteWhen": "#" }”
assignment-owner-changed AccessSeedAssignmentAtomicityTests.FailedAssignmentAuditLeavesNoCommittedAssignmentOrCompletionSignal “Assert.Equal() Failure: Values differ; Expected: 00000000-0000-0000-0000-000000000002; Actual: 0cee5498-1907-420a-895d-f14fb214484b”
assignment-saved-before-audit AccessSeedAssignmentAtomicityTests.FailedAssignmentAuditLeavesNoCommittedAssignmentOrCompletionSignal “Assert.Equal() Failure: Values differ; Expected: 0; Actual: 1”
cache-assignments AccessAuthorizationContractTests.AssignmentReadObservesIndependentCommitInTheSameScope “zero committed assignment rows must grant unrestricted scope after an independent deletion”
claim-removed-IIdentityProvider AccessOwnershipClaimTests.CommonIdentityPortRemainsOwnedByContractedAccess(type: "Cluckwork.Application.Common.IIdentityProvider") “Assert.Contains() Failure: Item not found in collection; Collection: ["Cluckwork.Application.Common.IStepUpGrantService"]; Not found: "Cluckwork.Application.Common.IIdentityProvider"”
claim-removed-IStepUpGrantService AccessOwnershipClaimTests.CommonIdentityPortRemainsOwnedByContractedAccess(type: "Cluckwork.Application.Common.IStepUpGrantService") “Assert.Contains() Failure: Item not found in collection; Collection: ["Cluckwork.Application.Common.IIdentityProvider"]; Not found: "Cluckwork.Application.Common.IStepUpGrantService"”
compat-DemoDataSeeder-MissingBaseDataAsync CompatibilityExceptionRealTreeTests.RealSourceTree_EveryCompatibilityExceptionIsRegistered “compatibility exception guard failed:; undeclared compatibility exception Cluckwork.Infrastructure.Persistence.DemoDataSeeder.MissingBaseDataAsync -> Access at src/Cluckwork.Infrastructure/Persistence/DemoDataSeeder.cs:214. Read through the module's contract, or add this row to compatibilityExceptions and fill in owner, reason and deleteWhen:; { "symbol": "Cluckwork.Infrastructure.Persistence.DemoDataSeeder.MissingBaseDataAsync", "reaches": "Access", "tables": ["AspNetRoles"], "owner": "", "reason": "", "deleteWhen": "#" }”
compat-SimulationDataSeeder-ComputeCountsAsync CompatibilityExceptionRealTreeTests.RealSourceTree_EveryCompatibilityExceptionIsRegistered “compatibility exception guard failed:; undeclared compatibility exception Cluckwork.Infrastructure.Persistence.SimulationDataSeeder.ComputeCountsAsync -> Access at src/Cluckwork.Infrastructure/Persistence/SimulationDataSeeder.cs:2217. Read through the module's contract, or add this row to compatibilityExceptions and fill in owner, reason and deleteWhen:; { "symbol": "Cluckwork.Infrastructure.Persistence.SimulationDataSeeder.ComputeCountsAsync", "reaches": "Access", "tables": ["AspNetUsers"], "owner": "", "reason": "", "deleteWhen": "#" }”
compat-SimulationDataSeeder-FindOwnerAsync CompatibilityExceptionRealTreeTests.RealSourceTree_EveryCompatibilityExceptionIsRegistered “compatibility exception guard failed:; undeclared compatibility exception Cluckwork.Infrastructure.Persistence.SimulationDataSeeder.FindOwnerAsync -> Access at src/Cluckwork.Infrastructure/Persistence/SimulationDataSeeder.cs:494. Read through the module's contract, or add this row to compatibilityExceptions and fill in owner, reason and deleteWhen:; { "symbol": "Cluckwork.Infrastructure.Persistence.SimulationDataSeeder.FindOwnerAsync", "reaches": "Access", "tables": ["AspNetRoles"], "owner": "", "reason": "", "deleteWhen": "#" }”
compat-SimulationDataSeeder-MissingBaseDataAsync CompatibilityExceptionRealTreeTests.RealSourceTree_EveryCompatibilityExceptionIsRegistered “compatibility exception guard failed:; undeclared compatibility exception Cluckwork.Infrastructure.Persistence.SimulationDataSeeder.MissingBaseDataAsync -> Access at src/Cluckwork.Infrastructure/Persistence/SimulationDataSeeder.cs:451. Read through the module's contract, or add this row to compatibilityExceptions and fill in owner, reason and deleteWhen:; { "symbol": "Cluckwork.Infrastructure.Persistence.SimulationDataSeeder.MissingBaseDataAsync", "reaches": "Access", "tables": ["AspNetRoles"], "owner": "", "reason": "", "deleteWhen": "#" }”
compat-SimulationDataSeeder-RestrictOneWorkerAsync CompatibilityExceptionRealTreeTests.RealSourceTree_EveryCompatibilityExceptionIsRegistered “compatibility exception guard failed:; undeclared compatibility exception Cluckwork.Infrastructure.Persistence.SimulationDataSeeder.RestrictOneWorkerAsync -> Access at src/Cluckwork.Infrastructure/Persistence/SimulationDataSeeder.cs:980. Read through the module's contract, or add this row to compatibilityExceptions and fill in owner, reason and deleteWhen:; { "symbol": "Cluckwork.Infrastructure.Persistence.SimulationDataSeeder.RestrictOneWorkerAsync", "reaches": "Access", "tables": ["UserRoleAssignments"], "owner": "", "reason": "", "deleteWhen": "#" }”
compat-UserRoleAssignmentRepository-ListByNameByUserAsync CompatibilityExceptionRealTreeTests.RealSourceTree_EveryCompatibilityExceptionIsRegistered “compatibility exception guard failed:; undeclared compatibility exception Cluckwork.Infrastructure.Repositories.UserRoleAssignmentRepository.ListByNameByUserAsync -> FlockManagement at src/Cluckwork.Infrastructure/Repositories/UserRoleAssignmentRepository.cs:40. Read through the module's contract, or add this row to compatibilityExceptions and fill in owner, reason and deleteWhen:; { "symbol": "Cluckwork.Infrastructure.Repositories.UserRoleAssignmentRepository.ListByNameByUserAsync", "reaches": "FlockManagement", "tables": ["Flocks"], "owner": "", "reason": "", "deleteWhen": "#" }”
completion-before-validation AccessSeedAssignmentAtomicityTests.FailedAssignmentAuditLeavesNoCommittedAssignmentOrCompletionSignal “Assert.Null() Failure: Value of type 'Nullable<DateTimeOffset>' has a value; Expected: null; Actual: 2026-10-04T04:17:33.2887820+00:00”
demo-fabricates-owner-roles AccessSeederActorTests.DemoSelectsTheLowestActiveOwnerAndResolvesTheirEntireRoleSet “Assert.Equal() Failure: Collections differ; ↓ (pos 1); Expected: ["Admin", "ReadOnly"]”
demo-null-owner-fallback AccessSeedOwnerFailureTests.MissingOrFailedOwnerReadNeverResolvesAnActorOrCertifiesFixtures(simulation: False, throwRead: False) “Assert.Equal() Failure: Values differ; Expected: Failed; Actual: Seeded”
demo-selects-disabled-owner AccessSeederActorTests.DemoSelectsTheLowestActiveOwnerAndResolvesTheirEntireRoleSet “Assert.Equal() Failure: Values differ; Expected: Seeded; Actual: Failed”
demo-selects-highest-owner AccessSeederActorTests.DemoSelectsTheLowestActiveOwnerAndResolvesTheirEntireRoleSet “Assert.Equal() Failure: Values differ; Expected: 00000000-0000-0000-0000-000000000002; Actual: ffffffff-ffff-ffff-ffff-ffffffffffff”
disabled-cast-reused SimulationDisabledCastMemberTests.SimulationSeed_WhenASeededCastMemberIsDisabled_RefusesTheRerun “Assert.Equal() Failure: Values differ; Expected: Failed; Actual: AlreadySeeded”
display-role-instead-of-live-role AccessAuthorizationContractTests.AssignmentAdmissionRefusesDisabledAndMultipleRoleTargets(disabled: True) “Assert.Equal() Failure: Values differ; Expected: UnprocessableEntity; Actual: Created”
drop-active-member-predicate ConfirmSaleOrderBoundaryTests.ConfirmReadsRoleAndAssignmentsAfterTheOrderLock(disableWorker: True) “Assert.Equal() Failure: Collections differ; Expected: [···, "order lock", "active actor"]; Actual: [···, "order lock", "active actor", "effective role", "assignments", "stock lock"]”
drop-role-account-predicate AccessLookupTests.GetEffectiveRole_AnswersOnlyForTheAccountItIsAskedAbout “Assert.Null() Failure: Value of type 'Nullable<EffectiveAccountRole>' has a value; Expected: null; Actual: Manager”
effective-role-before-proof AccessAuthorizationContractTests.MissingAssignmentProofReadsNoTargetsForKnownOrUnknownUsers “Assert.Empty() Failure: Collection was not empty; Collection: ["SELECT EXISTS (\n SELECT 1\n FROM "AspNetUs"···, "SELECT a0."Name"\nFROM "AspNetUserRoles" AS a"···, "SELECT EXISTS (\n SELECT 1\n FROM "AspNetUs"···]”
email-cancellation-dropped AccessSeedLookupTests.SeedReadsPropagateCancellation “Assert.ThrowsAny() Failure: No exception was thrown; Expected: typeof(System.OperationCanceledException)”
finance-IIdentityProvider PeerContractRealTreeTests.EveryModule_ReachesAContractedPeerOnlyThroughItsContractOrSeam “peer contract guard failed:; contract bypass Cluckwork.Application.Features.Expenses.AccessBypassProbe.Read -> Access through Cluckwork.Application.Common.IIdentityProvider at src/Cluckwork.Application/Features/Expenses/AccessBypassProbe.cs:2; Access declares a contract, so call one of its contract types”
finance-IStepUpGrantService PeerContractRealTreeTests.EveryModule_ReachesAContractedPeerOnlyThroughItsContractOrSeam “peer contract guard failed:; contract bypass Cluckwork.Application.Features.Expenses.AccessBypassProbe.Read -> Access through Cluckwork.Application.Common.IStepUpGrantService at src/Cluckwork.Application/Features/Expenses/AccessBypassProbe.cs:2; Access declares a contract, so call one of its contract types”
fixture-cancellation-dropped AccessSeedLookupTests.FixtureCreationAuthenticatesWithoutPasswordChangeAndWritesOnce(role: "Manager") “Assert.ThrowsAny() Failure: No exception was thrown; Expected: typeof(System.OperationCanceledException)”
fixture-cast-count-changed SimulationSeederTests.SimulationSeed_CastCountsAreTheOnesThePersonaAssertionsAssume “Assert.Equal() Failure: Values differ; Expected: 4; Actual: 3”
fixture-creation-twice AccessSeedLookupTests.FixtureCreationAuthenticatesWithoutPasswordChangeAndWritesOnce(role: "Manager") “Assert.True() Failure; Expected: True; Actual: False”
fixture-password-changed AccessSeedLookupTests.FixtureCreationAuthenticatesWithoutPasswordChangeAndWritesOnce(role: "Manager") “Assert.Equal() Failure: Values differ; Expected: OK; Actual: Unauthorized”
fixture-read-in-production AccessSeedDependencyTests.FixtureReadPortIsRegisteredOnlyOutsideProduction(environment: "Production", expected: False) “Assert.Equal() Failure: Values differ; Expected: False; Actual: True”
fixture-requires-password-change AccessSeedLookupTests.FixtureCreationAuthenticatesWithoutPasswordChangeAndWritesOnce(role: null) “Assert.Equal() Failure: Values differ; Expected: OK; Actual: Forbidden”
generic-adapter-access-bypass AdapterReachRealTreeTests.RealSourceTree_EveryAdapterReachIsDeclared “adapter reach guard failed:; contract bypass Cluckwork.Api.Endpoints.AccessBypassProbe.Read -> Access through Cluckwork.Application.Features.Users.IUserRoleAssignmentRepository at src/Cluckwork.Api/Endpoints/AccessBypassProbe.cs:2; Access declares a contract, so call one of its contract types; undeclared adapter reach Cluckwork.Api.Endpoints.AccessBypassProbe.Read -> Access through Cluckwork.Application.Features.Users.IUserRoleAssignmentRepository at src/Cluckwork.Api/Endpoints/AccessBypassProbe.cs:2; review and add the JSON row:”
generic-finance-access-bypass PeerContractRealTreeTests.EveryModule_ReachesAContractedPeerOnlyThroughItsContractOrSeam “peer contract guard failed:; contract bypass Cluckwork.Application.Features.Expenses.AccessBypassProbe.Read -> Access through Cluckwork.Application.Features.Users.IUserRoleAssignmentRepository at src/Cluckwork.Application/Features/Expenses/AccessBypassProbe.cs:2; Access declares a contract, so call one of its contract types”
identity-in-lifecycle AccessLifecycleDependencyTests.RealLifecycleVerbNeverConstructsIdentityRedisOrUserManager(verb: "rename-account", poisonRedis: True) “Assert.Equal() Failure: Values differ; Expected: 0; Actual: 1”
identity-in-seed-read AccessSeedDependencyTests.DemoSeederReadGraphNeverConstructsIdentityOrRedis(poisonRedis: True) “Assert.Null() Failure: Value is not null; Expected: null; Actual: System.InvalidOperationException: demo must not construct Redis”
lifecycle-usermanager-factory AccessLifecycleDependencyTests.RealLifecycleVerbNeverConstructsIdentityRedisOrUserManager(verb: "reactivate-account", poisonRedis: False) “Assert.Equal() Failure: Values differ; Expected: 0; Actual: 1”
members-cancellation-dropped AccessSeedLookupTests.SeedReadsPropagateCancellation “Assert.ThrowsAny() Failure: No exception was thrown; Expected: typeof(System.OperationCanceledException)”
normalizer-replaced-with-uppercase AccessSeedLookupTests.EmailReadUsesTheRegisteredIdentityNormalizer “Assert.NotNull() Failure: Value is null”
promoted-worker-reused SimulationPromotedWorkerTests.SimulationSeed_WhenTheRestrictedWorkerIsPromoted_RefusesTheRerun “Assert.Contains() Failure: Sub-string not found; String: "Simulation seed failed: Simulation seed c"···; Not found: "sim-worker-1@sim.local"”
pruned-seeder-access-reintroduced AdapterReachRealTreeTests.RealSourceTree_EveryAdapterReachIsDeclared “adapter reach guard failed:; undeclared adapter reach Cluckwork.Infrastructure.Persistence.SimulationDataSeeder.EffectiveHistoryDays -> Access through Cluckwork.Application.Features.Users.IAccessLookup at src/Cluckwork.Infrastructure/Persistence/SimulationDataSeeder.cs:122; review and add the JSON row:; [”
redis-in-lifecycle AccessLifecycleDependencyTests.RealLifecycleVerbNeverConstructsIdentityRedisOrUserManager(verb: "rename-account", poisonRedis: True) “Assert.Equal() Failure: Values differ; Expected: 0; Actual: 1”
redis-in-seed-read AccessSeedDependencyTests.DemoSeederReadGraphNeverConstructsIdentityOrRedis(poisonRedis: True) “Assert.Null() Failure: Value is not null; Expected: null; Actual: System.InvalidOperationException: demo must not construct Redis”
role-before-order-lock ConfirmSaleOrderBoundaryTests.ConfirmReadsRoleAndAssignmentsAfterTheOrderLock(disableWorker: True) “Assert.Equal() Failure: Collections differ; ↓ (pos 1); Expected: ["account lock", "order lock", "active actor"]”
role-members-hide-disabled AccessSeedLookupTests.RoleMembersIncludeDisabledUsersAreAccountScopedAndOrderedById “Assert.Equal() Failure: Collections differ; ↓ (pos 1); Expected: Guid[] [b8a2c54f-7062-48e0-84ed-0caef2bf6dea, e5af30bd-0a55-46a7-9b57-3c2a9d5df304]”
role-members-reverse-id-order AccessSeedLookupTests.RoleMembersIncludeDisabledUsersAreAccountScopedAndOrderedById “Assert.Equal() Failure: Collections differ; ↓ (pos 0); Expected: Guid[] [2e7e31de-1786-400e-af1c-f5ff6077e61c, d06ac68c-75b9-424c-be65-f1e4f594b390]”
second-actor-instance AccessAuthorizationContractTests.ScopedActorAndLookupShareStateAndRemainIndependent “Assert.Same() Failure: Values are not the same instance; Expected: CurrentUserContext { Email = "", IsResolved = False, Roles = [], UserId = 00000000-0000-0000-0000-000000000000 }; Actual: CurrentUserContext { Email = "", IsResolved = False, Roles = [], UserId = 00000000-0000-0000-0000-000000000000 }”
simulation-disabled-owner AccessSeedAssignmentAtomicityTests.FailedAssignmentAuditLeavesNoCommittedAssignmentOrCompletionSignal “Assert.Equal() Failure: Values differ; Expected: 00000000-0000-0000-0000-000000000002; Actual: 00000000-0000-0000-0000-000000000001”
simulation-highest-owner AccessSeedAssignmentAtomicityTests.FailedAssignmentAuditLeavesNoCommittedAssignmentOrCompletionSignal “Assert.Equal() Failure: Values differ; Expected: 00000000-0000-0000-0000-000000000002; Actual: ffffffff-ffff-ffff-ffff-ffffffffffff”
simulation-null-owner-fallback AccessSeedOwnerFailureTests.MissingOrFailedOwnerReadNeverResolvesAnActorOrCertifiesFixtures(simulation: True, throwRead: False) “Assert.Contains() Failure: Sub-string not found; String: "Simulation seed failed: Simulation seed c"···; Not found: "selected Owner cannot be read back"”
simulation-owner-literal-role AccessSeedAssignmentAtomicityTests.FailedAssignmentAuditLeavesNoCommittedAssignmentOrCompletionSignal “Assert.Equal() Failure: Collections differ; ↓ (pos 1); Expected: ["Admin", "ReadOnly"]”
singleton-actor AccessAuthorizationContractTests.ScopedActorAndLookupShareStateAndRemainIndependent “Assert.NotSame() Failure: Values are the same instance”
singleton-lookup AccessAuthorizationContractTests.ScopedActorAndLookupShareStateAndRemainIndependent “Assert.NotSame() Failure: Values are the same instance”
skip-exact-count-validation AccessSeedValidationTests.PollutedFixtureFailsExactValidationWithoutCompletionCertification “Assert.Equal() Failure: Values differ; Expected: Failed; Actual: Seeded”
swallow-assignment-failure AccessAuthorizationContractTests.AssignmentReadFailurePropagatesInsteadOfGrantingUnrestrictedScope “Assert.Throws() Failure: No exception was thrown; Expected: typeof(System.InvalidOperationException)”
target-before-proof AccessAuthorizationContractTests.MissingAssignmentProofReadsNoTargetsForKnownOrUnknownUsers “Assert.Empty() Failure: Collection was not empty; Collection: ["SELECT a."UserId", a0."Name"\nFROM "AspNetUse"···, "SELECT a."Id", a."Email", a."DisplayName", a"···, "SELECT a."UserId", a0."Name"\nFROM "AspNetUse"···, "SELECT a."Id", a."Email", a."DisplayName", a"···]”
worker-stored-as-role SimulationCrossDayRerunTests.Rerun_AfterUtcDayRollover_ConvergesToAlreadySeeded_WithUnchangedFingerprintAndNoDuplicateRows “Assert.Equal() Failure: Values differ; Expected: Seeded; Actual: Failed”
wrong-role-cast-reused SimulationReconfiguredCastTests.SimulationSeed_WhenACastPersonaLostItsRole_FailsNamingTheRole “Assert.Contains() Failure: Sub-string not found; String: "Simulation seed failed: Simulation seed c"···; Not found: "sim-manager-1@sim.local"”

E2 also completes #1023: #1033 supplied its schema, peer guard and Farm seam; this PR supplies both Access claims, the simulation prerequisite, current measurement and independent claim mutations. Its documented guard limits remain accepted. #858 P5 owns the five remaining fixture reads/write; #859 owns the filtered Flocks join.

Closes #1023

Closes #857

@mforce

mforce commented Oct 4, 2026

Copy link
Copy Markdown
Owner Author

Security review, PR #1060 (#857 E2) at 3731894a (Opus, read-only)

Verdict: no blocking security defect. Seeders and IAccessSeedLookup cannot be reached in Production. Actor reads are account-scoped. Roles come from the database, never a literal. Disabled Owners are skipped. No new code calls IgnoreQueryFilters. I found three P3 items, all latent, and one quality item the PR should fix before merge (Q1, stale ledger rows).

Security checklist

Area Result
Production reachability Safe. IAccessSeedLookup, DemoDataSeeder and SimulationDataSeeder are registered only inside !IsProduction(), and SeedCliCommand uses GetService. FixtureReadPortIsRegisteredOnlyOutsideProduction pins all three.
Same email in two farms Safe. FindUserByEmailAsync goes through AccountUserDirectory, which filters on AccountId and NormalizedEmail explicitly. ApplicationUser has no tenant filter, so that filter is the only guard. GetActorAsync filters on AccountId && Id. ActorReadsPreserve… covers both farms.
Disabled users Selection skips disabled Owners (lowest Id among active Owners). The cast path still refuses disabled members. See S3 for the reread.
Role fabrication Safe. Both seeders take actor.Roles from UserManager.GetRolesAsync on a fresh reread.
Owner selection Lowest Id among active Owners, with disabled Owners counted separately. If the reread misses, demo returns Failed and simulation throws, and neither falls back to another actor.
IgnoreQueryFilters Not used in new src code. The inlined RestrictOneWorkerAsync read matches UserRoleAssignmentRepository.ListByUserAsync exactly, and runs after tenant.Resolve (SimulationDataSeeder.cs:274), so the tenant filter applies.
CreateUserAsync credentials It forwards to IdentityProvider.CreateUserAsync with mustChangePassword:false, which is the same call as before. The password still goes through UserManager.CreateAsync validation, the role is checked against Roles.Assignable, and the audit row requires an actor (#500). The fixture password comes from Simulation:CastPassword, never a literal.
Contract bypass Claiming IIdentityProvider and IStepUpGrantService closes the direct route. A Finance probe and an adapter probe both go red, per the worker's mutants. Two contract ports remain wide; see S1 and S2.

Findings

S1 (P3, latent): IAccessOperations.CreateUserAsync lets any adapter that already reaches Access create a user without step-up.

  • Where: IAccessOperations.cs:20 and AccessOperations.cs:25. The port is registered unconditionally (CluckworkIdentityServiceCollectionExtensions.cs:142), so it exists in Production.
  • How it is exposed: interactive creation needs a fresh, single-use step-up grant (Require recent step-up authentication for privileged user administration #308/Step-up (#308) gates only Owner creation — a stolen Owner token still buys durable access #360, CreateUserHandler.cs:21). This method skips the grant and can assign any assignable role, Owner included.
  • Evidence: my mutant M3 added an IAccessOperations parameter to UserEndpoints.CreateUser. AdapterReachRealTreeTests, PeerContractRealTreeTests and ModuleLedgerRealTreeTests all stayed green (5/5). That happens because IAccessOperations is a contract type and the endpoint row already declares Access.
  • Not a regression: on main, IIdentityProvider was a free Platform port, so this capability was wider before. RecoverAdminAsync on the same port already has the same exposure.
  • Remedy: add a caller guard like FarmDirectoryCallerTests that limits IAccessOperations to Api/Cli and SimulationDataSeeder. Otherwise, state in the decision record that only review protects this port.

S2 (P3, latent): IAccessSeedLookup reads any farm's users and is in the adapter- and peer-visible contract.

  • Where: the port takes an arbitrary accountId and never compares it with TenantContext. Like IFarmDirectory, it reads across farms by design.
  • Why it is only latent: Production does not register it, so a stray endpoint fails to resolve rather than leaking data. In Development, Testing or Staging (!IsProduction()), an endpoint or peer that takes it would return another farm's user email, disabled state and roles.
  • Remedy: the same caller guard as S1 (limit it to the two seeders), or one sentence in 857-access-contract.md naming this as a review-only boundary.

S3 (P3, parity): the Owner reread returns DisabledAt, but neither seeder checks it.

  • Where: DemoDataSeeder.cs:145-148 and SimulationDataSeeder.cs:289-291.
  • Failure scenario: an Owner disabled between ListUsersInRoleAsync and GetActorAsync is still resolved as the fixture actor. main has the same window, because it reused the stale ApplicationUser.
  • Remedy: the reread now makes the fix one condition: if (actor is null || actor.DisabledAt is not null) fails the seed. This is a non-Production verb, so it is optional.

My E1 leftovers

  • S1, null meaning farm-wide for an unknown or foreign user id: addressed in documentation. IAccessLookup.cs:14-15 now says to pass the resolved actor or a verified tenant member, and that the read does not check membership. The decision record repeats it. The result type is unchanged, so the bare null remains (see Q3).
  • S2, AccessLifecycleDependencyTests does not poison UserManager: addressed by narrowing the claim. The test is unchanged. The decision record and the src/AGENTS.md rule now claim only "without Identity or Redis", which is the narrower option I offered.

Mutation spot-check

Mutant (mine) Result
M1: drop AccountId from GetActorAsync Red. AccessSeedLookupTests.ActorReadsPreserve…: Assert.Null() Failure
M2: demo resolves [Roles.Owner] instead of the actual roles Red. AccessSeederActorTests: collections differ
M3: endpoint takes IAccessOperations Green (S1)

All 61 of the worker's mutant logs report Failed!. The unclaimed-* controls pass on main, which is what they are meant to show. M1 and M2 repeat the worker's actor-loses-account-predicate and demo-fabricates-owner-roles and fail on the same assertions. Head runs, one class at a time under sg docker: AccessSeedLookupTests 6/6, AccessSeederActorTests 1/1, AccessSeedDependencyTests 4/4, AccessSeedOwnerFailureTests 4/4, AccessSeedAssignmentAtomicityTests 1/1, AccessLifecycleDependencyTests 7/7. Application guards (ownership claim, adapter reach, peer, module contract, compatibility, ledger, matrix) 12/12.

Code quality (P3, slice-scoped)

Thermo-nuclear

  • Q1 (fix before merge): 20 adapter rows in the ledger are stale. AdapterReachRealTreeTests reports 20 Loosenable SimulationDataSeeder.* -> Access rows. They went stale because this PR moved SimulationOptions and CurrentUserContext to Persistence. One of them, RolesOfAsync, names a method this PR deleted. That is why the coupling matrix drops from A (48) to A (28). Each stale row lets that seeder member start using Access types later without failing any check. Prune them; AGENTS.md says shrinking is free.
  • Q2 (option for [C] #514 slice 16: Platform composition, jobs, CLI and seeder conversion #858): duplicated Owner selection. Both seeders still carry their own copy of FindOwnerAsync, then call GetActorAsync. A single IAccessSeedLookup.SelectOwnerAsync returning (AccessActor?, disabledCount) would remove both copies and the separate reread. That is reasonable to defer to [C] #514 slice 16: Platform composition, jobs, CLI and seeder conversion #858, because the design fixes the reread at its original position for SQL-trace parity.
  • Q3 (option): nullable fields that cannot be null. AccessActor.Email and AccessUserSummary.Email are string?, but AspNetUsers.Email is NOT NULL (migration RequireUserIdentityColumns). Declaring them string removes three ! operators.
  • No file crosses 1,000 lines because of this PR: SimulationDataSeeder.cs was already 2,641 lines, and this PR made it shorter. No conditionals were added to shared paths.

Ponytail

  • module-ledger.json adapters: delete: the 20 Loosenable SimulationDataSeeder -> Access rows (Q1).
  • AccessSeedAssignmentAtomicityTests.cs:L60-80 and AccessSeederActorTests.cs:L20-40: reuse: the same three-Owner setup loop appears in both. Move it to one helper in IntegrationTests/Infrastructure.
  • AccessSeedAssignmentAtomicityTests.cs:L11: shrink: the Microsoft.AspNetCore.Identity using sits after Microsoft.Extensions.*. Sort it.

net: about -40 lines possible.

Evidence: /home/mforce/.cluckwork-slices/reviews/1060-opus-evidence/ (head runs, mut-M1..M3*.log, loosenable.log).

@mforce

mforce commented Oct 4, 2026

Copy link
Copy Markdown
Owner Author

Verdict: APPROVE E2 at 3731894a, base 20198660. No new product defect or slice-scoped P3 quality finding. This review supports closing #857 and the remaining #1023 implementation work. It does not claim that E2 fixes every E1 review note.

The requested E1 leftovers are:

Note Severity / status Scenario and evidence
Opus S1, unknown/foreign user receives unrestricted assignments Documented precondition; behavior unchanged src/Cluckwork.Application/Features/Users/IAccessLookup.cs:14 now requires a resolved actor or verified tenant member. AccessLookup.cs:31 still returns null when that id has no visible rows. A caller that skips membership verification could interpret this as authorization. I checked all three current callers: middleware and FlockScopeGuard use their resolved actor; confirm first verifies the live effective role after its locks. AccessLookupTests passed 6/6, including its explicit unknown-id and foreign-worker null assertions. E2 clarifies S1; it does not add a membership check.
Opus S2, lifecycle guard misses UserManager P3, inherited and unaddressed tests/Cluckwork.Api.IntegrationTests/AccessLifecycleDependencyTests.cs:76 poisons IIdentityProvider, while the other branch poisons Redis. I temporarily made the real lifecycle DI factory construct UserManager<ApplicationUser> before constructing AccessAccountLifecycle. All 7 cases still passed, including the six real-verb cases. A future UserManager dependency therefore remains undetected. Poison that dependency too if the intended invariant includes it. The current lifecycle constructor has no such dependency, and E2 accurately limits its documented guarantee to IdentityProvider/Redis.
Astra E1 P3, effective-role forwarding helper P3, inherited and unaddressed src/Cluckwork.Infrastructure/Identity/AccessLookup.cs:13 still forwards to the single-caller private method at line 43. Folding that body into the public method removes an unnecessary hop. There is no demonstrated product failure; this remains optional cleanup outside E2's changed implementation.

The 28-entry contract is complete for the agreed design and contains no aggregate or persistence leak. The assignment repository remains outside it. Both Common identity ports are independently Access-owned and outside the contract. Recursive-contract, peer, adapter, compatibility and generated-matrix checks pass. The matrix matches regeneration; Access adapter reach changes from 48 to 28 without adding or widening a peer W/R edge. Five Access compatibility rows expire at #858, and the unchanged filtered Flocks LEFT JOIN expires at #859. Live #1023/#858 amendments record those limits. The existing method-body, return/property/field and Platform-helper gaps remain explicitly outside #1023's guarantee.

Seeder behavior is preserved. The fixture lookup and both seeders are absent from Production registration. Demo's real read graph constructs neither IdentityProvider nor Redis. Independent tests cover the lowest active Owner, every actual role, same-email cross-farm actor reads, the registered normalizer, missing/failed rereads, cancellation, fixture creation, exact validation and assignment/audit atomicity. The simulation assignment retains the ordered untracked query and one save after its Owner-authored audit. Context/options bodies differ only by namespace.

I compared the supplied base/head snapshots directly: demo's 977 and simulation's 453 action/actor rows, worker rotation and the complete simulation manifest match. All 42 request artifacts match byte-for-byte. After normalizing only SQL parameter names, each seeder adds exactly the documented account/id-scoped Owner reread: demo 4389→4390 statements, simulation 1772→1773. The successful simulation fixture also passed its 39 existing cases independently, including counts, attribution, rotation, manifest and rerun checks. #1039's three query-warning checks pass; the integration initializer enables that policy for these seeder runs too.

Independent mutation spot-checks:

Temporary change Observed assertion failure
Finance takes List<IIdentityProvider> Named Access contract bypass. Removing only that claim makes the same probe pass.
Finance takes List<IStepUpGrantService> Independently fails for that port. Removing only its claim makes the same probe pass.
Add IUserRoleAssignmentRepository to the contract Recursive guard reports the returned UserRoleAssignment aggregate.
Remove simulation assignment's Access compatibility row Guard names RestrictOneWorkerAsync and UserRoleAssignments as undeclared.
Remove actor lookup's account predicate Foreign-account actor makes Assert.Null fail.
Save assignment before its audit Audit-failure test finds 1 committed assignment instead of 0.

Verification: 75 distinct unchanged-head tests passed across 15 targeted classes, zero skips. Six red mutations and two green ownership controls were reproduced; affected checks passed again after restoration. The UserManager probe's 7/7 green result is a confirmed guard limitation, not mutation coverage. Tests ran one class at a time with DOTNET_USE_POLLING_FILE_WATCHER=1, and Docker consumers used sg docker. No full integration suite was run. The worker's 374 targeted cases, 54-per-side parity cases and 62 mutation summaries were inspected as supplied evidence, not represented as independently rerun.

Applied pstack:thermo-nuclear-code-quality-review and ponytail-review to the E2 diff at P3 scope. No new structural regression: AccessSeedLookup is 36 lines; simulation shrinks 2644→2641; demo grows 427→429; no file crosses 1000 lines. The separate read port earns its purpose by avoiding demo's credential dependency graph. Ponytail result for E2: Lean already. Ship.

All scratch changes were restored and the checkout was clean before posting. No implementation fix, commit or push was made. Reproduction scripts, TRX/logs, comparisons and the detailed quality/verification reports are in /home/mforce/.cluckwork-slices/reviews/1060-3731894a-astra-evidence/.

@mforce

mforce commented Oct 4, 2026

Copy link
Copy Markdown
Owner Author

Reply to Astra's review, addressed in 1100445eafd13680113caf32a64ab914d4d8c9b3.

The lifecycle builder now poisons UserManager<ApplicationUser> alongside Identity/Redis. Your exact temporary factory—resolve UserManager, then ActivatorUtilities.CreateInstance<AccessAccountLifecycle>—reproduced 7/7 green before the fix, then failed all six real-verb cases:

RealLifecycleVerbNeverConstructsIdentityRedisOrUserManager
Assert.Equal() Failure: Values differ
Expected: 0
Actual:   1

After restoration, AccessLifecycleDependencyTests passes 7/7. The single-caller role helper is inlined; its query body is byte-identical, both TSV sites are rekeyed, and both query tokens remain 4adcdb84. Lookup passes 6/6, query warnings 3/3, and frozen B/D comparisons remain identical. Account/active-role and cache mutants were refreshed and fail assertions.

Combined fix validation: 722/722 Application tests; 87/87 cases across nine serial targeted classes; warning-free solution build; 66 named assertion-red mutation records. The PR body has the updated change map and quoted mutation table. Exact probe/control TRX and the final results are in /home/mforce/.cluckwork-slices/reviews/857-E2-evidence/. No PR merge was performed.

@mforce

mforce commented Oct 4, 2026

Copy link
Copy Markdown
Owner Author

Reply to Opus's review, addressed in 1100445eafd13680113caf32a64ab914d4d8c9b3.

Q1: removed exactly the 20 observed stale simulation→Access ceilings, including RolesOfAsync. Regenerated with CLUCKWORK_REGENERATE_MATRIX=1; the generated matrix needs no further delta. The final adapter report has zero Loosenable entries. An IAccessLookup parameter on EffectiveHistoryDays passes before the prune and now fails the named adapter assertion:

adapter reach guard failed:
undeclared adapter reach Cluckwork.Infrastructure.Persistence.SimulationDataSeeder.EffectiveHistoryDays -> Access

S1/S2: the decision record explicitly says only review restricts operations callers to operator CLI/simulation and fixture lookup callers to the two seeders. It names Production operations' step-up bypass and the fixture port's cross-farm reads outside Production. No caller guard is added. The lifecycle guard also poisons UserManager; the exact reviewer factory mutation now fails all six real-verb cases with Expected: 0, Actual: 1, and the restored class passes 7/7. S3/Q2/Q3 remain unchanged.

Combined fix validation: 722/722 Application tests; 87/87 cases in nine serial targeted classes; zero build warnings/errors; 66 named assertion-red mutations; unchanged frozen B/D traces. The role helper is inlined with its exact query body and unchanged TSV query tokens. The PR body records the final change map, properties and quoted failures. Evidence: /home/mforce/.cluckwork-slices/reviews/857-E2-evidence/. No PR merge was performed.

@mforce

mforce commented Oct 4, 2026

Copy link
Copy Markdown
Owner Author

Verdict: APPROVE 1100445e. No PRODUCT defect found in the fix round. Reviewed only 3731894a..1100445e; the supplied diff exactly matches Git and contains the six expected files.

Opus Q1 was valid. My first pass missed the unused adapter ceilings because the normal guard reports Loosenable without failing. This re-check explicitly asserted that the list is empty.

  • Ledger and matrix: the JSON diff removes exactly 20 SimulationDataSeeder -> Access adapter rows, including RolesOfAsync, and changes nothing else in the ledger. The real adapter walk passes with 402 adapters, 139 non-empty rows, zero unused ceilings and zero undeclared reaches. Module-edge and compatibility checks also pass. I reran CLUCKWORK_REGENERATE_MATRIX=1 dotnet test tests/Cluckwork.Application.Tests --filter FullyQualifiedName~CouplingMatrixRealTreeTests with the polling setting and normal build flags. Both cases pass; the generated matrix is byte-identical and git diff --exit-code is clean. No matrix delta is expected from removing unused ceilings.
  • UserManager gap closed: AccessLifecycleDependencyTests.cs:67 now poisons UserManager<ApplicationUser>. I reapplied my exact first-pass test-only factory probe: resolve UserManager, then construct AccessAccountLifecycle through ActivatorUtilities. All six real-verb cases now fail the named assertion with Expected: 0, Actual: 1; the unrelated provisioning case passes. After restoring the probe, the class passes 7/7.
  • Helper inlining is behavior-preserving: src/Cluckwork.Infrastructure/Identity/AccessLookup.cs:20 contains the former private method's byte-identical body. The entire assignment-read body is also byte-identical, including its unresolved-tenant refusal. Account/user/active predicates, user-scoped role lookup, untracked reads and cancellation forwarding remain intact. AccessLookupTests passes 6/6. The TSV differs only by ReadEffectiveRoleAsync becoming GetEffectiveRoleAsync; both query hashes remain 4adcdb84. TenantBypassRealTreeTests passes 4/4, including the live missing/stale classification check.
  • Caller limits are accurately documented: docs/decisions/857-access-contract.md:52 explicitly records Production operations' interactive step-up bypass and review-enforced caller restriction. Line 69 records the fixture lookup's cross-farm input and availability outside Production. These are documented limits, not new caller guards. The earlier unknown/foreign assignment-id precondition remains unchanged. No unrelated runtime change appears in the diff.

Independent validation: 25 passing cases across seven targeted classes, zero skips, plus the expected six assertion failures from the UserManager probe. Tests ran serially, one class at a time, with DOTNET_USE_POLLING_FILE_WATCHER=1; integration runs used sg docker. No full integration suite was run. The narrow quality check found no new P3 issue; the forwarding helper is gone.

All temporary test edits were restored. The checkout was clean at the reviewed head before posting. No product code, commit or push was made. Scripts, mutation diff, TRX/logs, byte comparisons and matrix hashes are retained in /home/mforce/.cluckwork-slices/reviews/1060-1100445e-astra-evidence/.

@mforce
mforce merged commit cd1de3a into main Oct 4, 2026
19 checks passed
@mforce
mforce deleted the chore/857e2-access-contract branch October 4, 2026 06:32
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.

test(arch): guard module-to-module calls against non-contract types [C] #514 slice 15: Access contract — security hold point, runs last

1 participant