Skip to content

feat: scope Worker reads to assigned flocks (#388) - #611

Merged
mforce merged 29 commits into
mainfrom
feat/388-flock-read-scoping
Aug 28, 2026
Merged

mforce merged 29 commits into
mainfrom
feat/388-flock-read-scoping

Conversation

@mforce

@mforce mforce commented Aug 27, 2026 •

Copy link
Copy Markdown
Owner

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 gets
422 FlockScope.NotAssigned). This PR extends the same boundary to reads:

  • A new FlockScope service + FlockScopeResolutionMiddleware resolves the
    acting user's flock scope per request from UserRoleAssignment rows (not a
    JWT 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.
  • 8 flock-scope query filters on AppDbContext scope Flock, EggLot,
    DailyEntry, BirdMovement, InventoryMovement, FeedUsage, WaterUsage
    and Expense reads. Unassigned flock detail is 404 (symmetric filtering,
    not 403).
  • EggLotRepository.GetAvailableFifoLockedAsync stays farm-wide for Worker
    SalesFlow (current main behavior; Configurable Worker sales allocation scope by flock assignment #612 owns a future configurable policy).
    The two AdminOnly reconciliation lock paths carry explicit flock predicates.
  • A fix commit preserves the write-side 422 FlockScope.NotAssigned contract
    while 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.ts carries the #277 read assertions
(the restricted picker hides the unassigned flock) while keeping the server-side
422 write-guard assertions, and FlockScopeTests gets a direct-EF fact that
isolates every one of the 8 combined query filters (AllEightCombinedFilters_ AssignedPresent_UnassignedAbsent_FarmWideVisible), so no child-row read is
masked 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 of
which 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-write
lookup 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 sale
confirmation 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 the
    flock-scope predicate from EggLotRepository.GetAvailableFifoLockedAsync
    only. GetByIdsLockedAsync and GetByDailyEntryLockedAsync (both AdminOnly
    reconciliation locks) keep their predicate. Replaced
    EggLotRawSqlPath_IsScoped with
    EggLotRawSqlPaths_HonorTheirDistinctSalesAndAdminContracts, one fact
    asserting all three raw-SQL contracts explicitly (FIFO farm-wide, both
    reconciliation locks flock-scoped).
  • test: prove elevated assignments never narrow reads (#388) — Owner/Manager
    tests now assert that an elevated role's own flock assignment never
    narrows its reads (AssertElevatedUserWithAssignmentIsUnrestricted): assign
    the elevated user to one of two flocks, confirm GET /api/v1/flocks still
    returns both.
  • docs: record farm-wide Worker FIFO allocation (#388) — corrected the
    design/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:

Mutation Change Test RED at
MR-FIFO re-add the removed scope predicate to GetAvailableFifoLockedAsync EggLotRawSqlPaths_HonorTheirDistinctSalesAndAdminContracts Assert.Contains(fifo, lotBId)
MR-BYIDS remove the predicate from GetByIdsLockedAsync same fact Assert.DoesNotContain(byIds, lotBId)
MR-BYENTRY remove the predicate from GetByDailyEntryLockedAsync same fact Assert.Empty(byUnassignedEntry)
MR-ELEVATED delete the Owner/Manager early-return in FlockScopeResolutionMiddleware (mutation only, not shipped) Owner_Request_Completes_ScopeUnrestricted / Manager_Request_Completes_ScopeUnrestricted both RED (only assigned flock visible)

All 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, full
suite — 1484/1484 passed), tools/schema-docs/generate.sh --check (up to
date), web typecheck, tools/simulation/ui (Playwright project) typecheck,
web test: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) — added
    FiveRewrittenFilters_TenantConjunctExcludesForeignRows_WhenFlockScopeUnrestricted
    to FlockScopeTests.cs. The existing combined-filter fact used same-account
    fixtures, so an accidental removal of the AccountId conjunct on
    BirdMovement/InventoryMovement/FeedUsage/WaterUsage/Expense would
    not 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 the
    tenant 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: InventoryLotRepository
    moved from the modified-file allow-list to do-not-touch (it has no FlockId
    column, so no predicate was ever added there); the Expense-filter mutation
    instruction now names the actual shipped assertion
    (ExpenseFilter_FarmWideVisible_UnassignedExcluded) instead of a wrong
    cross-reference to a Flock test; the claimed cross-transaction mid-request
    concurrency 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:

Mutation Filter RED at
MT-BIRDMOVEMENT BirdMovement Assert.DoesNotContain(foreignBirdId, birdIds)
MT-INVENTORYMOVEMENT InventoryMovement Assert.DoesNotContain(foreignInventoryId, inventoryIds)
MT-FEEDUSAGE FeedUsage Assert.DoesNotContain(foreignFeedId, feedIds)
MT-WATERUSAGE WaterUsage Assert.DoesNotContain(foreignWaterId, waterIds)
MT-EXPENSE Expense Assert.DoesNotContain(foreignExpenseId, expenseIds)

All five confirmed causal against a green baseline (FlockScopeTests:
10/10 passed before and after each restore); AppDbContext.cs diffs clean
against 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, full
suite — 1485/1485 passed), tools/schema-docs/generate.sh --check (up to
date), tools/simulation/ui (Playwright project) typecheck, web
test: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) — EggInventoryMovement has no FlockId column, so its Worker-reachable movement-ledger endpoint (GET /api/v1/stock/lots/{id}/movements) is protected by the filtered EggLot parent lookup in StockEndpoints.ListLotMovements, not by a filter on the movement table itself. Added ScopedWorker_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 a HelpPage.test.tsx fact (same withOverride shape 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: added FlockScopeResolutionMiddleware to the pipeline diagram between TenantResolutionMiddleware and CredentialEpochMiddleware, with a new placement-table row; corrected "Three positions" to "Four"; corrected the pipeline entry count to 30 (22 Use*, 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-PARENT mutation in Task 2's shipped-test inventory; added HelpPage.test.tsx to Task 4's files/evidence; corrected M-SUBMIT-TENANT's claimed result from 422 FlockScope.NotAssigned to the actual 422 UnprocessableEntity from later domain validation (Owner test actor, unrestricted by FlockScopeGuard) — the stable mutation claim is 404 changed to non-404.

New mutation, applied/rebuilt/confirmed RED, then restored/rebuilt green:

Mutation Change Test RED at
MP-PARENT delete the EggLot parent-gate if in StockEndpoints.ListLotMovements ScopedWorker_LotMovementLedger_UsesFilteredParentGate unassigned-lot response becomes 200 instead of the asserted 404

Confirmed causal against a green baseline (FlockScopeTests: 11/11 passed before and after); StockEndpoints.cs diffs 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), web test: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 only SubmitDailyEntryHandler.
The same race — a request-start FlockScope snapshot pinned to flock A, then
a SEPARATE transaction adding live assignment B before the handler runs —
applies equally to the three sibling flock-scoped write handlers:
RecordDailyEntryHandler, RecordFeedUsageHandler and RecordWaterUsageHandler,
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(), explicit AccountId reinstated, full natural key +
    non-Voided predicate preserved, Include(Grades)). After each handler's
    live flockScope.CheckAsync succeeds:
    • RecordDailyEntryHandler: flock lookup and natural-key lookup now use
      the scoped-write pair instead of flocks.GetByIdAsync /
      repository.FindByNaturalKeyAsync.
    • RecordFeedUsageHandler: BOTH the pre-transaction provenance flock
      lookup 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 lookup
      now 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.
    • Allowlisted DailyEntryRepository.FindByNaturalKeyForFlockScopedWriteAsync
      in tenant-bypass-allowlist.json with the same bypass justification as
      its GetByIdForFlockScopedWriteAsync sibling.
  • 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 FlockScope to
A, then in a SEPARATE DB scope adds live assignment B plus a feed item/lot.
While B is live, RecordDailyEntry/RecordFeedUsage/RecordWaterUsage all
succeed 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 additional
usage/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:

Mutation Change RED at
MC-RECORD-FLOCK RecordDailyEntryHandler flock lookup back to filtered GetByIdAsync Assert.True(dailyResult.IsSuccess)
MC-RECORD-NATURAL RecordDailyEntryHandler natural-key lookup back to filtered FindByNaturalKeyAsync duplicate-key DbUpdateException (stale lookup misses B's entry, creates a second one)
MC-FEED-LINK RecordFeedUsageHandler provenance flock lookup back to filtered Assert.Equal(entryBId, feedUsage.DailyEntryId) — actual null
MC-FEED-NATURAL RecordFeedUsageHandler provenance natural-key lookup back to filtered same assertion — actual null
MC-FEED-LIFECYCLE RecordFeedUsageHandler in-transaction flock lookup back to filtered Assert.True(feedResult.IsSuccess)
MC-WATER-FLOCK RecordWaterUsageHandler flock lookup back to filtered Assert.True(waterResult.IsSuccess)
MC-WATER-PROVENANCE RecordWaterUsageHandler natural-key lookup back to filtered Assert.Equal(entryBId, waterUsage.DailyEntryId) — actual null

All 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), web typecheck,
web build — all green. No web/ files changed in this round, so the SPA
E2E/Playwright surface is unaffected.

For review only — do not merge.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for security reviews. Please try again later.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

@mforce

mforce commented Aug 28, 2026

Copy link
Copy Markdown
Owner Author

@codex review again

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Breezy!

Reviewed commit: 2d132a1dcf

ℹ️ 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".

@mforce

mforce commented Aug 28, 2026

Copy link
Copy Markdown
Owner Author

@codex re-check please — pushed 40f7609 covering the three round-1 findings above:

  • P1 "Keep newly created flocks visible to scoped workers": FIXED — Owner/Manager-only flock creation now (API AuthPolicies.AdminOnly on POST /api/v1/flocks, plus admin-gated SPA triggers/dialogs on Flocks and Daily Entry). A Worker can no longer create a flock at all, so it can no longer create one that then disappears.
  • P2 "Preserve void access to every allocated lot": REFUTED — POST /api/v1/sales/{id}/void is AdminOnly, not SalesFlow; only always-unrestricted Owner/Manager reach the flock-scoped raw-SQL lock path.
  • P2 "Stop stale worker assignments from scoping other roles": tracked as follow-up Configurable Worker sales allocation scope by flock assignment #612 (fail-closed narrowing today, not a live hole); recorded on that issue.

Full detail + evidence in the inline replies on each thread (now resolved).

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

@mforce

mforce commented Aug 28, 2026

Copy link
Copy Markdown
Owner Author

@codex the P1 error-re-execution finding (r3878288969) is addressed in 00d9ba5 (fix+test) / afe0437 (docs) — see the thread reply for commit, causal test, and mutation-check evidence.

@chatgpt-codex-connector

Copy link
Copy Markdown

The P1 finding is addressed at afe0437e.

Testing

  • ⚠️ dotnet test tests/Cluckwork.Api.IntegrationTests/Cluckwork.Api.IntegrationTests.csproj -c Release --no-restore --filter 'FullyQualifiedName~FlockScopeMiddlewareTests.ErrorReExecution_SkipsAssignmentResolution_WithoutDatabaseAccess' (could not run because the current environment does not have dotnet installed)
  • ✅ git status --short --branch (clean working tree on branch work)

View task →

@mforce

mforce commented Aug 28, 2026

Copy link
Copy Markdown
Owner Author

Feature-driver convergence

Stop rule met on unchanged head fe0d250553806a79e60fcb910a3bab2c0ab0b668:

  • Clean round 1: Claude Opus — CLEAN; Codex gpt-5.6-sol — CLEAN.
  • Clean round 2: Claude Opus — CLEAN; Codex gpt-5.6-sol — CLEAN.
  • Driver final verification: Release build 0 warnings/errors; 1491/1491 .NET tests; canonical 44-row mutation ledger (1 GREEN control + 43 RED guards), including independently rerun final sibling-race and foreign-tenant bypass mutations.
  • Hosted CI, E2E, CodeQL, dependency review, image/Trivy, web, and GitGuardian all green.
  • All attached review threads dispositioned/resolved.

Loop stopped deliberately at 2 consecutive clean rounds on the unchanged final head. PR remains unmerged pending owner authorization.

@mforce

mforce commented Aug 28, 2026

Copy link
Copy Markdown
Owner Author

@codex 1 more final review please

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

Reviewed commit: fe0d250553

ℹ️ 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".

@mforce
mforce merged commit 5884a9a into main Aug 28, 2026
10 checks passed
@mforce
mforce deleted the feat/388-flock-read-scoping branch August 28, 2026 18:11
mforce pushed a commit that referenced this pull request Sep 12, 2026
🤖 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>
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.

Worker flock assignment is enforced on writes but not on reads — a restricted worker can enumerate and read unassigned flocks

1 participant