Skip to content

refactor(access): name flock assignments through IFlockLookup, removing the last compatibility exception - #1070

Merged
mforce merged 4 commits into
mainfrom
refactor/859-flocks-join
Oct 5, 2026
Merged

mforce merged 4 commits into
mainfrom
refactor/859-flocks-join

Conversation

@mforce

@mforce mforce commented Oct 4, 2026 •

Copy link
Copy Markdown
Owner

Why

The last #850 compatibility exception: UserRoleAssignmentRepository.ListByNameByUserAsync (Access) left-joined Flock Management's filtered Flocks set to name a user's flock assignments. The owner approved the fix on 2026-10-04. Access reads the assignments itself, then names them through Flock Management's contract IFlockLookup.GetDisplayNamesAsync, accepting one extra round trip. There is no name cache: names are per farm and per viewer, renames must show at once, and #271 allows one serving instance.

Closes #859

Change map

Start with AccessModule.ListFlockAssignmentsAsync, then the pin test.

File Change
src/Cluckwork.Infrastructure/Identity/AccessModule.cs ListFlockAssignmentsAsync reads ListByUserAsync, then calls IFlockLookup.GetDisplayNamesAsync once with the list's flock ids
src/Cluckwork.Infrastructure/Repositories/UserRoleAssignmentRepository.cs, IUserRoleAssignmentRepository.cs ListByNameByUserAsync and its join deleted
src/Cluckwork.Application/Features/Users/IAccessModule.cs UserFlockAssignment moves here, beside its only producer. Its old comment said a hidden flock's row gets a null flock id; it never did, and the pin now asserts the id is kept
ReferenceMarkers.cs, ShapeProbe.cs, UserEndpoints.cs the AssignmentProjection tag and the comment about the single join removed
RealModuleLedger.Exemptions.cs CompatibilityExceptions = []
RealModuleLedger.Edges.cs Access → FlockManagement names AccessModule. UserFlockAssignment leaves Access → Farm because its new file does not import Domain.Accounts
Architecture/Data/coupling-matrix.md regenerated: Access → Farm W (19) → W (18), Access → FlockManagement R (3) → R (4)
NamedRowProjectionTests.cs new pin FlockAssignmentList_IsIdenticalForEveryViewer; AssignmentProjection_IsASingleLeftJoinStatement becomes AssignmentNames_AreOneBoundedFlockReferenceRead; the Worker-scope test calls IAccessModule
docs/decisions/859-*.md, 858-*.md, src/AGENTS.md the obligation is recorded as discharged, with the SQL and mutation tables

src/AGENTS.md lines 28 and 30 named the join and the remaining exception; both now state that AccessModule uses IFlockLookup and that CompatibilityExceptions is empty. No #632 registry changes: neither BypassAllowList nor FilterFreeSetSites names either method, and the join used no IgnoreQueryFilters.

Output parity

The pin is commit 1 and passed against the join before anything moved. It asserts exact rows in assignment-id order, with ids that sort in neither insertion nor flock order:

Viewer Farm-wide Active Archived Depleted Missing flock Other farm's flock
Owner id null, name null named named named id kept, name null id kept, name null
Worker scoped to Archived id null, name null id kept, name null named id kept, name null id kept, name null id kept, name null

Another farm reading the same user gets no rows, and a user with no assignments gets no rows. Today's join and the new lookup both return a hidden flock's row with its flock id and a null name, never no row.

SQL, base and head

Captured from the pin's Owner and Worker calls.

Base, one statement:

SELECT u."Id", u."FlockId", f0."Name"
FROM "UserRoleAssignments" AS u
LEFT JOIN (
    SELECT f."Id", f."Name" FROM "Flocks" AS f
    WHERE f."AccountId" = @ef_filter__AccountId AND (@ef_filter__IsUnrestricted4 OR f."Id" = ANY (@ef_filter__AssignedFlockIds))
) AS f0 ON u."FlockId" = f0."Id"
WHERE u."AccountId" = @ef_filter__AccountId AND u."UserId" = @userId
ORDER BY u."Id"

Head, two statements:

SELECT u."Id", u."AccountId", u."CreatedAtUtc", u."FarmId", u."FlockId", u."HouseId", u."UserId"
FROM "UserRoleAssignments" AS u
WHERE u."AccountId" = @ef_filter__AccountId AND u."UserId" = @userId
ORDER BY u."Id"

SELECT f."Id", f."Name", f."Status"
FROM "Flocks" AS f
WHERE f."AccountId" = @ef_filter__AccountId AND (@ef_filter__IsUnrestricted3 OR f."Id" = ANY (@ef_filter__AssignedFlockIds)) AND f."Id" = ANY (@ids)

Every difference:

  • There is a second round trip, which the owner accepted.
  • The flock read applies the same AccountId AND flock-scope predicate. GetDisplayNamesAsync is plain LINQ over the filtered set with no IgnoreQueryFilters, so the visibility matches. It adds Id = ANY(@ids), bound to the list's distinct flock ids (5 in the pin's Owner case).
  • The assignment read selects the full row instead of two columns.
  • When no assignment names a flock, there is no second statement, because GetDisplayNamesAsync returns early on an empty id set.
  • The two statements do not share one snapshot. A flock renamed between them shows its newer name.

Mutations

Each was red, then restored.

Mutation Red Message
GetDisplayNamesAsync drops only the flock-scope filter (IgnoreQueryFilters with the tenant reinstated) pin, AssignmentProjection_RespectsFlockScopeForAWorker the scoped Worker sees the Depleted / Active names
One flock id left out of the lookup (Skip(1)) AssignmentNames_AreOneBoundedFlockReferenceRead, FlockAssignments_NameEachAssignmentAndLeaveFarmWideBlank, Worker-scope test expected 3 bound ids, actual 2 (the pin's skipped id was its missing flock, so the pin stayed green)
One lookup per row AssignmentNames_AreOneBoundedFlockReferenceRead three tagged flock reads
Exception row re-added CompatibilityExceptionRealTreeTests stale compatibility exception … (its trigger was #859)
Repository joins db.Flocks again CompatibilityExceptionRealTreeTests undeclared compatibility exception …ListByNameByUserAsync -> FlockManagement

Verification

All runs were local, one class at a time:

  • NamedRowProjectionTests: 25/25 at base with the pin, and 25/25 at head.
  • AccessAuthorizationContractTests, FlockScopeMiddlewareTests, FlockScopeTests, NamedEntityDiscoveryTests, RoleMatrixTests, SaleAllocationPolicyTests and StepUpAuthTests (every class that calls the route): 6, 13, 16, 53, 18, 13 and 45 passed, none failed.
  • Application.Tests Architecture, TenantBypass and Documentation: green (417 and 381 tests across the two filtered runs).
  • dotnet build Cluckwork.sln: 0 warnings.

No UI change, so there are no screenshots. The response body is byte-identical.

mforce added 3 commits October 4, 2026 23:28
Owner, flock-scoped Worker, another farm and a user with no assignments, over
farm-wide, Active, Archived, Depleted, missing and other-farm flock rows, in
assignment-id order. Passes against the current left join.
AccessModule reads the user's assignments, then names their flocks with one
IFlockLookup.GetDisplayNamesAsync read bounded to the list's flock ids. The
repository's left join to the filtered Flocks set is gone, and with it the last
compatibility exception. Access -> FlockManagement gains AccessModule; the
coupling matrix is regenerated.
@mforce

mforce commented Oct 4, 2026

Copy link
Copy Markdown
Owner Author

Review of record, including security, at f03c30a6 against main base bfe82116.

One P3 documentation finding. No correctness, security, or structural code-quality findings.

[P3] Update the canonical rules that still retain the deleted exception. src/AGENTS.md:28 still says the assignment repository's filtered Flocks LEFT JOIN retains #859; line 30 says the one remaining compatibility exception belongs to #859. Root AGENTS.md requires agents to read these rules before backend work, so the next routine assignment or registry change encounters instructions that contradict this implementation. Update both statements alongside the decision records, and correct the PR body's claim that src/AGENTS.md does not name the exception. This is a documentation correction, not a security blocker.

I independently checked the following:

  • The new pin is commit 9d39bbc1, directly after base bfe82116 and before production refactor 4459324a. I checked out that test-only commit and ran NamedRowProjectionTests: 25/25 against the old join. The class also passed 25/25 at head and after restoring the security mutation.
  • Both implementations retain every assignment in assignment-ID order. Owners see Active, Archived and Depleted names. A Worker scoped to Archived sees only that name. Hidden, missing and other-farm references keep their flock IDs with null names; farm-wide rows keep null ID/name. Other-farm viewers and users with zero assignments get no rows. The corrected hidden-ID comment matches the old join's actual a.FlockId projection.
  • The HTTP route remains Owner-only. The assignment read retains its tenant filter; the lookup retains AccountId AND flock-scope, with no IgnoreQueryFilters. Supplied IDs further restrict that filtered set and cannot widen visibility. The existing lookup deduplicates IDs and skips SQL for an empty set. No authorization decision depends on the returned name.
  • The two statements do not share a snapshot. A rename between them can yield the newer display name without changing authorization, IDs or ordering. This is consistent with the accepted extra read and current-name contract.
  • UserFlockAssignment correctly leaves the Access-to-Farm import-based cell; AccessModule correctly enters Access-to-FlockManagement as a read. Matrix regeneration passed with zero diff. The repository join and interface member are deleted, CompatibilityExceptions is empty, and its semantic guard passes. The [C] #514 slice 17: assembly split — conditional on Track A/B evidence #859 exception obligation is discharged in code. Neither affected read belongs to the Replace TenantBypass line-number anchors with stable query identities #632 bypass registries; both registries remain unchanged and their real-tree checks pass.

I reproduced both requested mutations myself:

Mutation Observed failure
GetDisplayNamesAsync uses IgnoreQueryFilters with the current tenant explicitly reinstated Both the parity pin and Worker-scope test fail on leaked same-tenant names. Restoring the lookup returns 25/25 to green.
Restore the exact exception row from base without restoring the join The compatibility guard fails with stale compatibility exception … (its trigger was #859). Restoring the empty registry returns 2/2 to green.

Final targeted runs, one class at a time:

Class Passed
NamedRowProjectionTests 25
AccessAuthorizationContractTests 6
FlockScopeMiddlewareTests 13
FlockScopeTests 16
RoleMatrixTests 18
CompatibilityExceptionRealTreeTests 2
CouplingMatrixRealTreeTests 2
ModuleLedgerRealTreeTests 2
TenantBypassRealTreeTests 4
SeamSurfaceRealAssemblyTests 2

All runs used DOTNET_USE_POLLING_FILE_WATCHER=1; integration ran through sg docker -c against real PostgreSQL. No full integration suite was run. The checkout is restored and clean.

Applied pstack:thermo-nuclear-code-quality-review and ponytail-review, scoped to this diff through P3. The implementation reuses the existing port, removes 41 production lines net, and adds no cache or abstraction. The second read depends on the first, so sequential execution is appropriate. No file newly crosses 1,000 lines. Ponytail: Lean already. Ship.

@mforce

mforce commented Oct 4, 2026

Copy link
Copy Markdown
Owner Author

Fixed the P3 in 938d6e45. Docs only.

Verification: Application.Tests Documentation passed 20/20, and Architecture + TenantBypass passed 417/417. The tracked-file guards (PostgresImagePin/RedisImagePin_IsOneIdenticalStringAcrossEveryTrackedFile, SchemaDocsTests) passed 4/4.

@mforce
mforce merged commit 4e9bd1c into main Oct 5, 2026
19 checks passed
@mforce
mforce deleted the refactor/859-flocks-join branch October 5, 2026 00:09
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.

[C] #514 slice 17: assembly split — conditional on Track A/B evidence

1 participant