Repository navigation
feat: scope Worker reads to assigned flocks (#388) - #611
Conversation
Also allow-lists the DailyEntryRepository.GetByIdForFlockScopedWriteAsync IgnoreQueryFilters site on the tenant-bypass guard (companion to the fix commit; keeps the full suite green).
|
You have reached your Codex usage limits for security reviews. Please try again later. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 232e28cb59
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@codex review again |
|
Codex Review: Didn't find any major issues. Breezy! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
|
@codex re-check please — pushed 40f7609 covering the three round-1 findings above:
Full detail + evidence in the inline replies on each thread (now resolved). |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 40f76091e0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
The P1 finding is addressed at
Testing
|
Feature-driver convergenceStop rule met on unchanged head
Loop stopped deliberately at 2 consecutive clean rounds on the unchanged final head. PR remains unmerged pending owner authorization. |
|
@codex 1 more final review please |
|
Codex Review: Didn't find any major issues. Can't wait for the next one! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
🤖 I have created a release *beep* *boop* --- ## [0.1.0](v0.0.4...v0.1.0) (2026-09-12) ### ⚠ BREAKING CHANGES * log in by farm code, with per-account email identity ([#532](#532)) (#564) ### Features * **accounts:** add Account.Slug (farm code), suspend/reactivate, list-accounts verb ([#531](#531)) ([3fe9754](3fe9754)) * **accounts:** provision additional farms ([#581](#581)) ([006f298](006f298)) * add Aspire local development AppHost ([#567](#567)) ([2c9e6b9](2c9e6b9)) * add configurable worker sale allocation ([#619](#619)) ([0955095](0955095)) * add searchable entity pickers ([#642](#642)) ([60d2053](60d2053)) * **api:** provision-account takes an optional --timezone at creation ([#603](#603)) ([#694](#694)) ([a0aee39](a0aee39)) * **audit:** show the sales-line audit payload as a readable Details column ([#745](#745)) ([#749](#749)) ([d26d389](d26d389)) * **auth:** add ApplicationUser.StepUpLogoutEpoch column ([#338](#338)) ([#554](#554)) ([18306ee](18306ee)) * certify over-cap simulation fixture bands ([#633](#633)) ([a67b2e1](a67b2e1)), closes [#627](#627) * **cli:** rename-account verb to change a farm code ([#732](#732)) ([#733](#733)) ([4b70559](4b70559)) * **customers:** edit existing customer details ([#625](#625)) ([#626](#626)) ([062a55c](062a55c)) * **jobs:** single-runner leader gate for the durable job worker ([#271](#271)) ([#555](#555)) ([4148f9b](4148f9b)) * let owners change user email addresses ([#605](#605)) ([842347b](842347b)) * log in by farm code, with per-account email identity ([#532](#532)) ([#564](#564)) ([68adb62](68adb62)) * **ratelimit:** distributed IP-keyed auth limiters ([#544](#544)) ([#558](#558)) ([ec14972](ec14972)) * **ratelimit:** distributed per-account report concurrency cap with local-ceiling fallback ([#545](#545)) ([#559](#559)) ([1522e4e](1522e4e)) * **sales:** mark discounted lines, total the discount, and show it in the Orders list ([#723](#723), [#724](#724)) ([#741](#741)) ([1a07441](1a07441)) * **sales:** record list, old and new price in the order-line audit payload ([#722](#722)) ([#742](#742)) ([97c866f](97c866f)) * **sales:** refuse an over-ceiling confirm from a Sales user ([#727](#727)) ([#766](#766)) ([8c0792a](8c0792a)) * **sales:** show what each order still owes, and filter the list to unpaid ([#771](#771)) ([ca59d68](ca59d68)) * **sales:** snapshot the list price on the order line and show the discount ([#734](#734)) ([cffed5e](cffed5e)) * **sales:** snapshot the product name and unit in the order-line audit payload ([#747](#747)) ([#748](#748)) ([0481c06](0481c06)) * scope Worker reads to assigned flocks ([#388](#388)) ([#611](#611)) ([5884a9a](5884a9a)) * shared-state ports with Redis + in-process fallback ([#543](#543)) ([#552](#552)) ([f767fa9](f767fa9)) * suspend-account / reactivate-account operator verbs ([#534](#534)) ([#573](#573)) ([d0be26c](d0be26c)) * **tenancy:** write-side tenant guard + single-assignment TenantContext ([#546](#546)) ([#561](#561)) ([f371f1d](f371f1d)) * **web:** dashboard rework — capture-status tiles, 14-day trend, stock as a stacked bar ([#654](#654)) ([396ba23](396ba23)) * **web:** date-range filters on audit and expenses, and the stock lot filter gets its bounded toolbar ([#666](#666), [#667](#667), [#653](#653)) ([94b188f](94b188f)) * **web:** elevation hierarchy and sentence-case labels ([#651](#651), [#652](#652)) ([#661](#661)) ([28db4c7](28db4c7)) * **web:** Expenses and Audit keep a clear-filters control while rows are still showing ([#679](#679)) ([#697](#697)) ([b859982](b859982)) * **web:** expenses filters by a date range like its sibling screens ([#667](#667)) ([f13858f](f13858f)) * **web:** key the farm brand palette per farm ([#586](#586)) ([#600](#600)) ([7183a43](7183a43)) * **web:** let operators forget remembered farms ([#598](#598)) ([577d94e](577d94e)) * **web:** one-line provenance, bounded date filters, and empty states that invite action ([#653](#653), [#655](#655)) ([#668](#668)) ([80b53f4](80b53f4)) * **web:** prefill the farm code from ?farm= and remember it ([#535](#535)) ([#588](#588)) ([b7f5cc6](b7f5cc6)) * **web:** split authenticated routes into lazy chunks ([#620](#620)) ([5089271](5089271)) * **web:** the audit log filters by a date range, and says which window is empty ([#666](#666)) ([63027e0](63027e0)) * **web:** typeset numbers as numbers and refresh the Help glossary ([#650](#650), [#657](#657)) ([af4fe11](af4fe11)) ### Bug fixes * **api:** order same-instant audit events by a durable monotonic key ([#700](#700)) ([8fcf084](8fcf084)) * **api:** print the farm code from bootstrap-admin ([#589](#589)) ([#594](#594)) ([34032ac](34032ac)) * **audit:** show the price a line sold for, not its list price ([#759](#759)) ([e6b37d0](e6b37d0)) * **audit:** store catalog enums by name and guard the add-item transaction shape ([#751](#751)) ([23609ff](23609ff)) * **auth:** reject invalid account claims ([#622](#622)) ([8d6c7fe](8d6c7fe)) * **auth:** require step-up for durable user access ([#360](#360)) ([#607](#607)) ([f767dce](f767dce)) * **ci:** bound the npm audit calls and give the web job room to finish ([#686](#686)) ([153b7a8](153b7a8)) * **ci:** escalate the audit bound to SIGKILL, so it actually bounds ([#686](#686)) ([a0c8f4e](a0c8f4e)) * **ci:** fail closed on invalid vulnerability config ([#621](#621)) ([1690db8](1690db8)) * **ci:** lockfix covers the two AppHost lock files, derived from the sln ([efb05e6](efb05e6)) * **ci:** lockfix covers the two AppHost lock files, derived from the sln ([8986d77](8986d77)) * **ci:** remove invalid XML comment from nuget.lockfix.config ([#541](#541)) ([5f1bc0a](5f1bc0a)) * **ci:** the advisory vuln gate no longer blocks on an unusable report ([#686](#686)) ([aaf6934](aaf6934)) * **ci:** the advisory vuln gate no longer blocks on an unusable report ([#686](#686)) ([64f1f53](64f1f53)) * **i18n:** tl help text names the saleable flag and unit-system setting what their labels call them ([#688](#688)) ([#696](#696)) ([bfd24d7](bfd24d7)) * **infra:** AccountId must be a non-nullable Guid or both tenant write layers refuse ([#673](#673)) ([#695](#695)) ([2470c4e](2470c4e)) * require step-up for flock scope changes ([#609](#609)) ([4151f89](4151f89)) * **sales:** keep a line's discount markers agreeing while its price is edited ([#752](#752)) ([#753](#753)) ([c159b4b](c159b4b)) * **sales:** say which kind of missing list price a line has ([#774](#774)) ([489180e](489180e)) * scope legacy logout to selected farm ([#624](#624)) ([fae8d82](fae8d82)) * **seed:** drain the daily-entry lock sweep so deep simulation fixtures validate ([#644](#644)) ([730fa23](730fa23)), closes [#638](#638) * **tenancy:** AccountId is a concurrency token, so the database refuses a detached cross-tenant write ([#562](#562)) ([4d1dfa3](4d1dfa3)) * **tenancy:** AspNetUserRoles carries a tenant column, so a role write naming another farm's user is refused ([#670](#670)) ([fc0552a](fc0552a)) * **tests:** bump the image-pin allow-list counts for the AppHost LocalPorts tests ([#593](#593)) ([58d3056](58d3056)) * **tests:** the OTLP collector survives a lost port race and ignores traffic that is not an export ([#672](#672), [#676](#676)) ([#677](#677)) ([965c737](965c737)) * **web:** a scoped audit view filtered to nothing names both the record and the range ([#666](#666)) ([41bbfe1](41bbfe1)) * **web:** an abandoned dialog attempt's success no longer hijacks the replacement on Customers, Daily Entry, Flocks, Grades and Products ([#703](#703)) ([#705](#705)) ([85605db](85605db)) * **web:** an abandoned dialog attempt's success no longer hijacks the replacement on Inventory, Expenses, History and Stock ([#703](#703)) ([#706](#706)) ([60a4997](60a4997)) * **web:** an abandoned edit's success no longer hijacks the dialog that replaced it on Users ([#703](#703)) ([#710](#710)) ([778faab](778faab)) * **web:** an abandoned order attempt's success no longer hijacks the dialog that replaced it ([#702](#702)) ([522c699](522c699)) * **web:** capture screens open on the flock you last used, and assigning one no longer guesses ([#646](#646)) ([#699](#699)) ([7f8f317](7f8f317)) * **web:** constrain dialog session helpers to declared scopes ([#715](#715)) ([389e3c8](389e3c8)) * **web:** date validation gets one boundary table instead of one case per review round ([#666](#666)) ([215f830](215f830)) * **web:** keep a paged window and an item panel on the user's newest intent ([#645](#645)) ([d81bccf](d81bccf)) * **web:** keep Sales order panels closed after pending writes ([#711](#711)) ([f0f7492](f0f7492)) * **web:** keep Sales panels closed after pending Open reads ([#716](#716)) ([620411f](620411f)) * **web:** make login take the cross-tab cookie lock so a racing refresh cannot restore the wrong session ([#648](#648)) ([ff18beb](ff18beb)) * **web:** make the entity picker read as a search field and focus it on open ([#736](#736)) ([66ef667](66ef667)), closes [#735](#735) * **web:** page truncated customer and movement tables with usePagedList ([7cfe4d6](7cfe4d6)) * **web:** reconcile Sales line edits with refreshed orders ([#717](#717)) ([d7dd2c9](d7dd2c9)) * **web:** the audit date filter accepts low-numbered years, and its empty state covers every narrowing ([#666](#666)) ([af52d25](af52d25)) * **web:** the audit date filter rejects impossible dates, and its history guard actually guards ([#666](#666)) ([8d51846](8d51846)) * **web:** the expense range bounds are not capped at today, which the month-end default exceeds ([#667](#667)) ([7e01864](7e01864)) * **web:** the help text calls the expiry field what the field calls itself ([#666](#666)) ([2fd1f3c](2fd1f3c)) * **web:** the stock lot date range sits in the bounded toolbar ([#653](#653)) ([43dec5e](43dec5e)) ### Refactoring * **web:** extract SalesPage's dialog-write wrapper into a shared useDialogAction hook ([#703](#703)) ([#704](#704)) ([60ee9d9](60ee9d9)) ### Documentation * add k6 preparation steps to the dev-database fixture runbook ([#643](#643)) ([a4f1f09](a4f1f09)) * add runbook for loading the simulation fixture into a dev database ([#639](#639)) ([2d143b8](2d143b8)) * **agents:** a PR closes its issue from the body, not the title ([#744](#744)) ([39be13c](39be13c)) * **agents:** drop the commit and push gate, and require screenshots on UI changes ([#757](#757)) ([6225172](6225172)) * **agents:** find guards by grepping registry readers; amend issues a PR overtakes ([#580](#580)) ([fe3fde8](fe3fde8)) * **agents:** the Playwright specs have been in CI since 2026-08-08 ([#768](#768)) ([68ee612](68ee612)) * **aspire:** record the second local database and pin the AppHost dashboard ports ([#623](#623)) ([713b941](713b941)) * compress AGENTS.md to one paragraph per rule, and draw the two orders that matter ([#551](#551)) ([997ae8a](997ae8a)) * item 7 names each screen's actual initial filter value ([#666](#666)) ([70a53d8](70a53d8)) * multi-farm tenancy decision record and AGENTS/GLOSSARY sync ([#537](#537)) ([#601](#601)) ([2c34771](2c34771)) * name the scoped filtered-empty key and state the [#653](#653) relationship plainly ([#666](#666)) ([0e93dac](0e93dac)) * note that a PackageReference in Directory.Build.props is invisible to the dependency graph ([4845724](4845724)) * **plans:** commit the [#722](#722) and [#745](#745) design records ([#754](#754)) ([c942fcd](c942fcd)) * record [#579](#579) as won't-fix — suspension is immediate for use, not issuance ([#582](#582)) ([7a3be40](7a3be40)) * record the [#508](#508) audit ordering key and the tracked-file guard lesson ([#701](#701)) ([08964e9](08964e9)) * **runbooks:** add procedure to rename the default farm's code after upgrade ([#731](#731)) ([2f6e242](2f6e242)) * screenshots of the running SPA in the README ([#550](#550)) ([711488a](711488a)) * **sim:** commit the dashboard screenshot, capture the palette matrix, and record the [#651](https://github.com/mforce/cluckwork/issues/651)/[#652](https://github.com/mforce/cluckwork/issues/652) conventions ([#660](#660), [#662](#662), [#663](#663), [#664](#664)) ([#665](#665)) ([930ea30](930ea30)) * specify searchable entity picker ([#641](#641)) ([91d4300](91d4300)) * split the README into audience-scoped docs and adopt repo-template scaffolding ([#548](#548)) ([b3f3fcf](b3f3fcf)) * surface Aspire local development workflow ([#568](#568)) ([a343baa](a343baa)) * **web:** record the per-screen idempotency-key policies and runWrite's refresh contract ([#703](#703)) ([#707](#707)) ([8bee651](8bee651)) * **web:** the date-cap help text covers every stocked item, not only feed ([#666](#666), [#667](#667)) ([c8433c5](c8433c5)) * **web:** the help text claims only what is true of recording, and says nothing about filter caps ([#666](#666), [#667](#667)) ([e2f63d1](e2f63d1)) * **web:** the help text describes the date-range filters that shipped ([#666](#666), [#667](#667)) ([c3275b7](c3275b7)) * **web:** the help text stops describing a cap the filters no longer have ([#666](#666), [#667](#667)) ([49654cd](49654cd)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: cluckwork-lockfix[bot] <309265648+cluckwork-lockfix[bot]@users.noreply.github.com>
Closes #388
Design doc:
docs/designs/388-worker-flock-read-scoping.md#388 is the read-side half of the flock-scope boundary. Writes were already
guarded by
FlockScopeGuard(a Worker submitting to an unassigned flock gets422 FlockScope.NotAssigned). This PR extends the same boundary to reads:FlockScopeservice +FlockScopeResolutionMiddlewareresolves theacting user's flock scope per request from
UserRoleAssignmentrows (not aJWT claim). Owner/Manager are unrestricted; a Worker with 0 assignments or a
farm-wide (no-flock) row is unrestricted too; otherwise restricted to the
assigned flock ids.
AppDbContextscopeFlock,EggLot,DailyEntry,BirdMovement,InventoryMovement,FeedUsage,WaterUsageand
Expensereads. Unassigned flock detail is 404 (symmetric filtering,not 403).
EggLotRepository.GetAvailableFifoLockedAsyncstays farm-wide for WorkerSalesFlow(currentmainbehavior; Configurable Worker sales allocation scope by flock assignment #612 owns a future configurable policy).The two AdminOnly reconciliation lock paths carry explicit flock predicates.
FlockScope.NotAssignedcontractwhile the read filter is active (the submit handler now uses a scoped-write
lookup that bypasses the filter but reinstates
AccountId).tools/simulation/ui/specs/worker.spec.tscarries the #277 read assertions(the restricted picker hides the unassigned flock) while keeping the server-side
422 write-guard assertions, and
FlockScopeTestsgets a direct-EF fact thatisolates every one of the 8 combined query filters (
AllEightCombinedFilters_ AssignedPresent_UnassignedAbsent_FarmWideVisible), so no child-row read ismasked by a parent flock lookup.
The mutation table covers M0-M5 from the design, plus the 8 flock-scope filter
rows (
MF-FLOCK,MF-DAILYENTRY,MF-EGGLOT,MF-BIRDMOVEMENT,MF-INVENTORYMOVEMENT,MF-FEEDUSAGE,MF-WATERUSAGE,MF-EXPENSE), each ofwhich removes ONLY the flock conjunct (tenancy retained) and is caught by the
new fact at that entity's exact unassigned-row assertion, and two write-side
rows (
M-SUBMIT,M-SUBMIT-TENANT) that pin the submit handler's scoped-writelookup so a filtered read can't mask the 422 guard as a 404 and the
query-filter bypass can't leak a foreign-account draft.
Review round 1 fixes (2026-08-27)
Owner decision:
main's farm-wide FIFO allocation for a Worker's saleconfirmation is preserved — sale confirmation is SalesFlow, not AdminOnly, and
a plain Worker may confirm a sale and allocate against the farm's full FIFO
stock, not only assigned-flock stock. #612 owns the future configurable
policy (assigned-only vs. farm-wide, its insufficient-stock warning, and any
migration/settings UI). This PR does not implement that flag.
fix: keep Worker sales allocation farm-wide (#388)— removed theflock-scope predicate from
EggLotRepository.GetAvailableFifoLockedAsynconly.
GetByIdsLockedAsyncandGetByDailyEntryLockedAsync(both AdminOnlyreconciliation locks) keep their predicate. Replaced
EggLotRawSqlPath_IsScopedwithEggLotRawSqlPaths_HonorTheirDistinctSalesAndAdminContracts, one factasserting all three raw-SQL contracts explicitly (FIFO farm-wide, both
reconciliation locks flock-scoped).
test: prove elevated assignments never narrow reads (#388)— Owner/Managertests now assert that an elevated role's own flock assignment never
narrows its reads (
AssertElevatedUserWithAssignmentIsUnrestricted): assignthe elevated user to one of two flocks, confirm
GET /api/v1/flocksstillreturns both.
docs: record farm-wide Worker FIFO allocation (#388)— corrected thedesign/plan docs to remove the stale "sale confirm/void are AdminOnly" and
"all three raw-SQL paths are flock-scoped" claims, and recorded the owner
decision + corrected mutation rows.
New mutation rows, each applied/rebuilt/confirmed RED at the named assertion,
then restored/rebuilt green:
MR-FIFOGetAvailableFifoLockedAsyncEggLotRawSqlPaths_HonorTheirDistinctSalesAndAdminContractsAssert.Contains(fifo, lotBId)MR-BYIDSGetByIdsLockedAsyncAssert.DoesNotContain(byIds, lotBId)MR-BYENTRYGetByDailyEntryLockedAsyncAssert.Empty(byUnassignedEntry)MR-ELEVATEDFlockScopeResolutionMiddleware(mutation only, not shipped)Owner_Request_Completes_ScopeUnrestricted/Manager_Request_Completes_ScopeUnrestrictedAll four confirmed causal against a green baseline; solution restored and
rebuilt clean,
grep -rn MUTANT src/ tests/returns nothing.Final gates run after the fix:
dotnet build/dotnet test(Release, fullsuite — 1484/1484 passed),
tools/schema-docs/generate.sh --check(up todate),
webtypecheck,tools/simulation/ui(Playwright project) typecheck,webtest:coverage,build,verify:sw— all green.For review only — do not merge.
Review round 2 fixes (2026-08-27)
Mechanical guard/docs increment — no product behavior changed. Driver fix
budget was 0.
test: isolate flock-filter tenant conjuncts (#388)(4282426) — addedFiveRewrittenFilters_TenantConjunctExcludesForeignRows_WhenFlockScopeUnrestrictedto
FlockScopeTests.cs. The existing combined-filter fact used same-accountfixtures, so an accidental removal of the
AccountIdconjunct onBirdMovement/InventoryMovement/FeedUsage/WaterUsage/Expensewouldnot have failed it — flock scoping alone still excluded the unassigned-flock
row. The new fact seeds a foreign-account row with an unrestricted
FlockScope, so the flock disjunct is true for every row and only thetenant conjunct can reject the foreign fixture — isolating exactly the
conjunct these five filters were rewritten on.
docs: correct flock-scope verification claims (#388)(7c0dc786) —corrected stale/wrong claims in the design and plan docs:
InventoryLotRepositorymoved from the modified-file allow-list to do-not-touch (it has no
FlockIdcolumn, so no predicate was ever added there); the Expense-filter mutation
instruction now names the actual shipped assertion
(
ExpenseFilter_FarmWideVisible_UnassignedExcluded) instead of a wrongcross-reference to a
Flocktest; the claimed cross-transaction mid-requestconcurrency test was replaced with the two facts that actually ship
(
Resolve_DifferingScope_ThrowsReassignmentException,RoleMatrixTests.FlockScope_CoversFeed_SubmitOfUnassignedDraft_AndMismatchedUnassign);the authorization map now states sale confirm is SalesFlow, sale void
is AdminOnly (previously conflated), and Payment views are SalesAccess,
excluding plain Workers.
Five isolated mutations, each removing only the tenant conjunct
(
e.AccountId == tenant.AccountId &&) from one of the five rewritten filters,applied one at a time against a green baseline, rebuilt, run against only the
new fact, confirmed RED at that entity's
Assert.DoesNotContain(foreign...Id),then restored/rebuilt/confirmed green before the next:
MT-BIRDMOVEMENTBirdMovementAssert.DoesNotContain(foreignBirdId, birdIds)MT-INVENTORYMOVEMENTInventoryMovementAssert.DoesNotContain(foreignInventoryId, inventoryIds)MT-FEEDUSAGEFeedUsageAssert.DoesNotContain(foreignFeedId, feedIds)MT-WATERUSAGEWaterUsageAssert.DoesNotContain(foreignWaterId, waterIds)MT-EXPENSEExpenseAssert.DoesNotContain(foreignExpenseId, expenseIds)All five confirmed causal against a green baseline (
FlockScopeTests:10/10 passed before and after each restore);
AppDbContext.csdiffs cleanagainst the pre-mutation original after the final restore, no MUTANT markers
left in
src//tests/.Final gates run after the fix:
dotnet build/dotnet test(Release, fullsuite — 1485/1485 passed),
tools/schema-docs/generate.sh --check(up todate),
tools/simulation/ui(Playwright project) typecheck,webtest:coverage(1978/1978 passed),build,verify:sw— all green.For review only — do not merge.
Clean round 1 fixes (2026-08-27)
Mechanical guard/docs increment — no product behavior changed. Driver fix budget was 0.
test: guard flock scope on egg movement ledgers (#388)(ca1f9fde) —EggInventoryMovementhas noFlockIdcolumn, so its Worker-reachable movement-ledger endpoint (GET /api/v1/stock/lots/{id}/movements) is protected by the filteredEggLotparent lookup inStockEndpoints.ListLotMovements, not by a filter on the movement table itself. AddedScopedWorker_LotMovementLedger_UsesFilteredParentGate: an assigned lot's movements return 200 and contain the seeded row; an unassigned lot's movements return 404.test(web): render flock-scoping Help glossary row (#388)(865c26ed) — added aHelpPage.test.tsxfact (samewithOverrideshape as the FIFO/step-up/Disabled-user guards) proving the flock-scoping glossary row reads its term and definition from the i18n catalog, not a hardcoded literal.docs: align flock-scope routes and middleware order (#388)(2d132a1d) —docs/architecture.md: addedFlockScopeResolutionMiddlewareto the pipeline diagram betweenTenantResolutionMiddlewareandCredentialEpochMiddleware, with a new placement-table row; corrected "Three positions" to "Four"; corrected the pipeline entry count to 30 (22Use*, seven terminal/map registrations,app.Run()).docs/plans/388-worker-flock-read-scoping.md: corrected the authorization/read map — Stock summary/lots/movement-ledger reads and Inventory item/lot/movement/usage reads are default-authenticated and Worker-reachable, not AdminOnly (only the stock movement write and inventory configuration/corrections are AdminOnly); rewrote Task 3's Inventory exclusion note (item/lot reads are Worker-reachable but farm-wide; movement/usage reads are Worker-reachable and protected by Task 2's EF filters); recorded the new movement-ledger fact +MP-PARENTmutation in Task 2's shipped-test inventory; addedHelpPage.test.tsxto Task 4's files/evidence; correctedM-SUBMIT-TENANT's claimed result from422 FlockScope.NotAssignedto the actual422 UnprocessableEntityfrom later domain validation (Owner test actor, unrestricted byFlockScopeGuard) — the stable mutation claim is 404 changed to non-404.New mutation, applied/rebuilt/confirmed RED, then restored/rebuilt green:
MP-PARENTEggLotparent-gateifinStockEndpoints.ListLotMovementsScopedWorker_LotMovementLedger_UsesFilteredParentGate200instead of the asserted404Confirmed causal against a green baseline (
FlockScopeTests: 11/11 passed before and after);StockEndpoints.csdiffs clean against the pre-mutation original after restore,grep -rn MUTANT src/ tests/returns nothing.Final gates run after the fix:
dotnet build/dotnet test(Release, full suite — 1486/1486 passed),tools/schema-docs/generate.sh --check(up to date),HelpPage.test.tsx(40/40 passed) +catalogParity.test.ts(192/192 passed),webtest:coverage(1979/1979 passed),build,verify:sw,tools/simulation/ui(Playwright project) typecheck — all green.For review only — do not merge.
Sibling write handler race fix (2026-08-28)
Codex final-review FIX-NOW: Submit's stale request-scope/live-assignment fix
(
MC-ASSIGNMENT-RACE, prior round) covered onlySubmitDailyEntryHandler.The same race — a request-start
FlockScopesnapshot pinned to flock A, thena SEPARATE transaction adding live assignment B before the handler runs —
applies equally to the three sibling flock-scoped write handlers:
RecordDailyEntryHandler,RecordFeedUsageHandlerandRecordWaterUsageHandler,including their natural-key and provenance reads.
fix: use live assignments across scoped write handlers (#388)(ef8fe9c7)— added
IDailyEntryRepository.FindByNaturalKeyForFlockScopedWriteAsync(same bypass shape as the existing
GetByIdForFlockScopedWriteAsync:IgnoreQueryFilters(), explicitAccountIdreinstated, full natural key +non-Voided predicate preserved,
Include(Grades)). After each handler'slive
flockScope.CheckAsyncsucceeds:RecordDailyEntryHandler: flock lookup and natural-key lookup now usethe scoped-write pair instead of
flocks.GetByIdAsync/repository.FindByNaturalKeyAsync.RecordFeedUsageHandler: BOTH the pre-transaction provenance flocklookup and the in-transaction lifecycle flock lookup, plus the
natural-key lookup, now use the scoped-write pair.
RecordWaterUsageHandler: the flock lookup and the natural-key lookupnow use the scoped-write pair.
Guard order, transaction order, item/lot lock order, and lifecycle rules
are unchanged — only which repository method resolves flock/entry state
after the live guard.
DailyEntryRepository.FindByNaturalKeyForFlockScopedWriteAsyncin
tenant-bypass-allowlist.jsonwith the same bypass justification asits
GetByIdForFlockScopedWriteAsyncsibling.docs: record scoped write assignment-race guards (#388)(e8847484) —corrected the plan's mutation ledger and added a "Sibling write handlers"
note so the doc no longer implies only Submit handles the assignment-
snapshot race.
New causal integration fact,
FlockScopeTests.ScopedWrites_AssignmentAddedAfterScopeSnapshot_UseLiveFlockAndNaturalKeyState:seeds flock A/B with explicit houses, pins a request scope's
FlockScopetoA, then in a SEPARATE DB scope adds live assignment B plus a feed item/lot.
While B is live,
RecordDailyEntry/RecordFeedUsage/RecordWaterUsageallsucceed against flock B's existing natural-key state from the still-pinned-to-A
scope (proven from a fresh unrestricted context, never the stale handler
context). B is then archived in another separate scope; a NEW request scope
still pinned to A invokes all three handlers again — each fails with its
exact lifecycle code (
DailyEntry.FlockNotActive,FeedUsage.FlockNotActive,WaterUsage.FlockNotActive), and a final fresh context proves no additionalusage/water rows and no further lot consumption.
Seven mutations, each applied one at a time against a green baseline,
rebuilt, run against only the new fact, confirmed RED, then
restored/rebuilt/confirmed green before the next:
MC-RECORD-FLOCKRecordDailyEntryHandlerflock lookup back to filteredGetByIdAsyncAssert.True(dailyResult.IsSuccess)MC-RECORD-NATURALRecordDailyEntryHandlernatural-key lookup back to filteredFindByNaturalKeyAsyncDbUpdateException(stale lookup misses B's entry, creates a second one)MC-FEED-LINKRecordFeedUsageHandlerprovenance flock lookup back to filteredAssert.Equal(entryBId, feedUsage.DailyEntryId)— actualnullMC-FEED-NATURALRecordFeedUsageHandlerprovenance natural-key lookup back to filterednullMC-FEED-LIFECYCLERecordFeedUsageHandlerin-transaction flock lookup back to filteredAssert.True(feedResult.IsSuccess)MC-WATER-FLOCKRecordWaterUsageHandlerflock lookup back to filteredAssert.True(waterResult.IsSuccess)MC-WATER-PROVENANCERecordWaterUsageHandlernatural-key lookup back to filteredAssert.Equal(entryBId, waterUsage.DailyEntryId)— actualnullAll seven confirmed causal against a green baseline; solution restored and
rebuilt clean, no leftover mutation state.
Final gates run after the fix:
dotnet build/dotnet test(full solution —2012/2012 passed, including 1490/1490
Cluckwork.Api.IntegrationTests),tools/schema-docs/generate.sh --check(up to date),webtypecheck,webbuild— all green. Noweb/files changed in this round, so the SPAE2E/Playwright surface is unaffected.
For review only — do not merge.