Skip to content

refactor(repositories): delete the generic repository base and its unused members - #1042

Merged
mforce merged 5 commits into
mainfrom
refactor/858-p2-generic-repository
Oct 3, 2026
Merged

mforce merged 5 commits into
mainfrom
refactor/858-p2-generic-repository

Conversation

@mforce

@mforce mforce commented Oct 3, 2026 •

Copy link
Copy Markdown
Owner

Nothing changes for users. No SQL statement, route or response changes: every deleted member had no caller.

For maintainers, the generic IRepository<T,TId> is gone. It made all 15 module repository ports expose GetByIdAsync, AddAsync, Update and Remove, whether or not anything used them. Each port now declares only what its callers use, so a handler can no longer Remove a sales order, a payment or an egg lot through its port. The append-only ledgers drop their throwing stubs: their ports simply have no Update or Remove.

This is P2 of #858's approved plan (plan comment). It also carries the two #1040 review fixes as a separate docs commit.

Refs #858

Fix round (39151656, on main 75e2b1be merged in as 0fbc3ca6)

Tests and comments only; no production change.

  • Astra F1 / Opus F1. The append-only exemption now also refuses a listed ledger that declares Update or Remove. The writer regex gained a named edit group, so the check reuses the existing discovery data. The empty-read and stale-entry checks are unchanged. N1 and M1 below are red.
  • Opus F2 to F4. The ISalesOrderAllocationRepository comment and specs/technical/tech_spec.md no longer name IRepository. The guard's comments say "never fed to a write" and state the AddAsync rule instead of [C] #514 slice 16: Platform composition, jobs, CLI and seeder conversion #858 history.
  • Not fixed: Opus O1 / M2. EggUnitConversionRepository.GetByIdAsync feeds a write, but the repository declares no AddAsync, Update or Remove, so the guard never sees it. The same goes for ProductEggGradeMapping, whose writer is AddMappingAsync. This predates the PR and does not fit in one line of the guard, so it stays a follow-up.
  • After the fix round: Application 702 of 702; TrackedMutationReadTests, SchemaDocsTests and QueryShapeWarningTests 9 of 9.

Commits

  1. e8e46067 docs(arch). These are the test(arch): declare the Insights module contract in the ledger #1040 review findings, docs only:
  2. 3117bdbd refactor(repositories). The deletion, described below.

How the members were chosen

The compiler did the search. I deleted IRepository.cs, built the whole solution, and read every CS1061 error. Each error names a port member a caller still uses.

Port Re-declared
IFlockRepository GetByIdAsync, AddAsync, Update (4 lifecycle handlers call it)
ICustomerRepository, IDailyEntryRepository, IEggGradeRepository, IEggLotRepository, IExpenseCategoryRepository, IExpenseRepository, IInventoryItemRepository, IPaymentRepository, ISalesOrderRepository, IWaterUsageRepository GetByIdAsync, AddAsync
IBirdMovementRepository, IFeedUsageRepository, IInventoryLotRepository, IInventoryMovementRepository AddAsync

A codemod (p2-prune.py, on the worker host) then deleted each implementation and test-fake member that its port no longer declares:

  • 15 Remove: 12 real deletes plus 3 throwing stubs.
  • 14 Update: 11 real plus 3 stubs.
  • 4 GetByIdAsync.

The whole solution then built with 0 errors under warnings-as-errors, which proves nothing called a deleted member on a concrete type either.

Kept on purpose, per the design review:

  • IFlockRepository.Update.
  • The live Remove on UserRoleAssignmentRepository and FarmLogoRepository. Neither ever derived from IRepository, so this PR does not touch them.

The guard that had to change

TrackedMutationReadTests.AllMutableRepositoryReads_AreTracked keeps every repository read that feeds a write path tracked (#561). It found those repositories by their public void Update/Remove methods. Those were mostly the dead members this PR deletes. Without a change, the guard fails closed: Expected to discover many mutable repositories, found 3: FarmLogoRepository.cs, FlockRepository.cs, UserRoleAssignmentRepository.cs.

  • Discovery now also keys on AddAsync. It walks 18 repository files instead of 16. CatalogRepositories.cs and EggInventoryMovementRepository.cs join, so ProductRepository.GetByIdAsync is now guarded too. Every read the guard checked on main is still checked, apart from the four dead GetByIdAsync this PR deletes.
  • Four append-only ledgers are excluded by name, like its existing ReadOnly exclusion: bird movements, egg inventory movements, feed usage and inventory movements. They add rows but have no single-entity read. The exclusion fails if a listed ledger gains such a read, or if a listed file stops being discovered.

Two other test edits keep their coverage:

  • SeamSurfaceTests. Its "repository inside a contract result" fixture now inherits a test-local IStore<T,TId> with the same four members.
  • AdapterReachTests:447. Its syntax-only fixture string says IStore<Flock, Guid>, so nobody reads it as the deleted production type. The test is about generic-argument resolution, and the name is irrelevant to it.

The two real-assembly guards anchor on typeof(IUnitOfWork).Assembly.

Change map

BEFORE
 Application.Common.IRepository<T,TId>  { GetByIdAsync, AddAsync, Update, Remove }
   ^ inherited by 15 ports in 7 modules (Commerce, Egg Operations, Finance, Flock Management, General Inventory)
 15 Infrastructure repositories implement all four; 3 append-only ones throw from Update/Remove
 8 Application test fakes and 1 integration decorator implement all four
 TrackedMutationReadTests discovers repositories by Update/Remove (16 files)

AFTER
 IRepository deleted; each port declares only the members its callers use (table above)
 implementations, fakes and the decorator lose 33 unused members
 TrackedMutationReadTests discovers by AddAsync/Update/Remove (18 files), 4 append-only ledgers excluded by name
 ledger, matrix, adapters and contracts unchanged (no type in a contract list referenced IRepository)

Files #857 also touches: none of #1037's files. SaleAllocationPolicyTests.cs is on #857 B's required-green list. This PR deletes two forwarding lines (Update, Remove) from its CountingEggLotRepository decorator and changes no assertion. If #857 measures "security suites unedited" against a baseline, take it after this PR or count those two lines as mechanical.

Mutation table

Each mutant was an exact single-match replacement, run, then reverted. "main" runs used a detached checkout of 922b208d.

# Mutant Head main Failing assertion, quoted
A1 CustomerRepository.GetByIdAsync reads AsNoTracking() (Customer lost its Update/Remove here) red (red: Customer was discovered via Update) These repositories can mutate, but the read that feeds the write path opts out of change tracking. … CustomerRepository.cs: GetByIdAsync
A2 ProductRepository.GetByIdAsync reads AsNoTracking() red green CatalogRepositories.cs: GetByIdAsync (newly guarded)
A3 FeedUsageRepository gains a GetByIdAsync red n/a FeedUsageRepository.cs is listed as an append-only ledger but now has a single-entity read (GetByIdAsync). Remove it from the list.
A4 FeedUsageRepository.cs dropped from the append-only list red n/a FeedUsageRepository.cs mutates [FeedUsage] but no tracked single-entity read was found for it. The guard's pattern no longer matches this file — fix the pattern, do not ignore it.
A5 A stale RetiredLedgerRepository.cs added to the list red n/a Assert.Contains() Failure: Item not found in collection … Not found: "RetiredLedgerRepository.cs"
B1 SeamSurfaceScanner stops walking a result's inherited interfaces red, 3 tests red, the same 3 tests Contract_RepositoryInAResult_IsAViolation: Assert.Single() Failure: The collection was empty. Same tests at head and main, so the IStore fixture keeps the coverage.
C1 UpdateCustomerHandler calls customers.Remove(customer) build fails builds error CS7036 (no Remove on ICustomerRepository; the compiler finds only an unrelated extension)
N1 (Astra, fix round) IFeedUsageRepository and FeedUsageRepository gain a real Remove(FeedUsage) (and the test fake implements it) red at 39151656; green at 3117bdbd (Astra's run) FeedUsageRepository.cs is listed as an append-only ledger but now declares Remove. Remove it from the list so its reads are checked like any other mutable repository.
M1 (Opus, fix round) FeedUsageRepository gains a real Update(FeedUsage) red at 39151656; green at 3117bdbd (Opus's run) … now declares Update. …
All reverted green

A1 on main was not run; the guard discovered CustomerRepository.cs there through its Update.

Merged main at fd8960e4 (#1039)

02293613 merges main (not a rebase). #1039 changed InventoryItemRepository, InventoryLotRepository, SalesOrderRepository and AccountRepository from FirstOrDefaultAsync to SingleOrDefaultAsync. The merge was clean, and all five of #1039's SingleOrDefaultAsync lines are kept. None of them sat in a member this PR deletes: the InventoryLotRepository change is in GetByIdLockedAsync, not the dead GetByIdAsync. After the merge I reran everything below with #1039's query-shape initializer on (a module initializer, so every integration test in that assembly throws on the listed EF warnings).

Test counts

Suite main 922b208d head before merge 3117bdbd head after merge 02293613
Whole-solution build (warnings as errors) 0 errors 0 errors 0 errors
Cluckwork.Application.Tests (full) 702 702 702
Integration, targeted: TrackedMutationReadTests, SaleAllocationPolicyTests, DetachedTenantWriteTests, FlockManagementTests, BirdMovementTests, FeedUsageTests, SchemaDocsTests — 48 of 48 48 of 48
The same plus #1039's QueryShapeWarningTests — — 51 of 51

The full integration suite was not run locally, to spare memory while #857 runs in parallel. CI runs it.

Deslop record

/deslop over git diff origin/main removed nothing more. The diff is deletions, one-line re-declarations, the guard's discovery change, and comments that state a reason:

  • the append-only port comments, rewritten from "the implementation throws" to "the port has no Update or Remove";
  • the test-local IStore comment;
  • the guard's discovery comment.

dotnet format turned out not to flag IDE0005 here (a planted unused using passed). The 14 removed using Cluckwork.Application.Common; lines were checked by grep: none of those files names a Common type any more.

Try to refute

  • No deleted member had a caller. The whole solution builds, tests included, so no call site remains.
  • The tracked-read guard lost no read it checked before. On main it checked 16 files' single-entity reads. At head it checks every one of those, except the four dead GetByIdAsync this PR deletes, plus ProductRepository.GetByIdAsync.
  • The append-only exclusion cannot hide a real read. A listed ledger that gains one fails (A3).
  • No runtime path changed. IFlockRepository.Update is the only Update still called, and it is kept. Handlers that change an entity load it through the tracked GetByIdAsync and save through IUnitOfWork, as before.

mforce added 3 commits October 3, 2026 17:13
IInsightsModule inherits IReportQueries and IExportQueries. Listing either
in the ledger would let any adapter or peer inject it past the ledger guards
and skip the facade, so the rule paragraph now says to keep both off.
It also notes that the Insights facade is registered in Program.cs until
#858 P7, not beside its handlers. Review findings F1 and F2 on #1040.

Refs #858
…used members

IRepository<T,TId> forced every module repository to expose GetByIdAsync,
AddAsync, Update and Remove. The compiler, run over the whole solution,
shows which ones callers use: AddAsync on all 15 ports, GetByIdAsync on 11,
and Update only on IFlockRepository. Each port now declares just those, and
the implementations and test fakes lose the members nothing called. That
removes 12 unreviewed Remove paths and 11 Update paths on aggregates whose
rules say void, never delete, and the three append-only ledgers no longer
need throwing stubs: their ports simply have no Update or Remove.

TrackedMutationReadTests found its repositories by their Update and Remove
methods, so it now also keys on AddAsync. It walks 18 repository files
instead of 16 (the catalog and egg-movement repositories join), and four
append-only ledgers with no single-entity read are excluded by name.
The seam fixture keeps its inherited-generic case with a test-local
IStore<T,TId>, and the two assembly anchors use IUnitOfWork.

Refs #858
@mforce

mforce commented Oct 3, 2026

Copy link
Copy Markdown
Owner Author

Independent review (Claude Opus 5.5: correctness, thermo-nuclear and ponytail) at head 3117bdbd. Posted by the coordinator.

PR #1042 review (Opus): delete IRepository<T,TId> and its unused members

Head 3117bdbd, base main 922b208d. Reviewed in a separate detached checkout. I formed this view before I read the PR body or Astra's review.

Product defect

None. No deleted member has a caller, by reflection or by any other route. Every runtime path is unchanged. All findings below are P3: test strength, stale prose, or PR scope.

Correctness pass

Was anything live deleted?

No. Evidence:

  • Static callers. The whole solution builds at head with 0 warnings and 0 errors (dotnet build Cluckwork.sln, warnings-as-errors).
  • Implementers. I grepped src, tests and tools for every implementer of the 15 narrowed ports. There are 15 production repositories, 8 Application fakes and 1 integration decorator (CountingEggLotRepository). The diff touches every one, so no orphaned Update, Remove or GetByIdAsync stays on a concrete type. The compiler cannot catch an orphan like that, because an extra member compiles.
  • Reflection. Only SeamSurfaceScanner, InsightsReadOnlyTests and ServingGuardCoverageTests call GetMethod(s). None of them looks up Update, Remove or IRepository by name.
  • DI. No open-generic IRepository<,> registration ever existed. git grep IRepository -- src tests tools now returns only one comment (F2).
  • Seeders, CLI verbs and jobs. The build covers them.
  • The re-declared members are all live in production. Every re-declared GetByIdAsync, AddAsync and Update has at least one src/ caller. The thinnest are EggOperationsModule.cs:85 (lots.GetByIdAsync) and UpdateWaterUsageHandler.cs:23. None survives only because a test calls it.
  • IFlockRepository.Update. All 4 lifecycle handlers call it, so keeping it is correct.
  • The append-only stubs. No test asserted their NotSupportedException. Removing the members from the port turns a runtime throw into a compile error, which is stronger.

Did each guard keep its purpose?

Guard Verdict Evidence
SeamSurfaceRealAssemblyTests anchor → typeof(IUnitOfWork).Assembly Kept 2/2 green. The ≥30-interface floor still holds, which shows the anchor names the Application assembly.
ModuleContractRealAssemblyTests anchor Kept 1/1 green.
SeamSurfaceTests repository-in-result fixture (test-local IStore<T,TId>) Kept 39/39 green. The scanner reaches IFlockStore only through resolved.GetInterfaces() (SeamSurfaceScanner.cs:318), because Type.GetMethods() on an interface omits inherited members. The fixture still exercises that inherited-interface branch, and the shape is structural, not name-based.
AdapterReachTests:447 generic-argument test Kept 97/97 green. The test is a syntax walk, so the container type was never bound to anything. The rename is cosmetic.
TrackedMutationReadTests.AllMutableRepositoryReads_AreTracked Kept, wider than before See the walk below.
Ledger, matrix and peer guards (ModuleLedgerRealTreeTests, CouplingMatrixRealTreeTests, PeerContractRealTreeTests, InsightsReadOnlyTests) Unaffected All green.

The tracked-read walk. I replayed the guard's own regexes in Python over git show <ref>:<file> for both refs (/tmp/opus1042_walk.py):

  • main discovers 16 files.
  • Head discovers 18 files: the same 16 plus CatalogRepositories.cs (Product) and EggInventoryMovementRepository.cs.
  • Reads. Every read checked on main is still checked at head, except the 4 GetByIdAsync members this PR deletes (bird movement, feed usage, inventory lot, inventory movement). ProductRepository.GetByIdAsync is newly checked.

The PR's "superset" claim is CONFIRMED.

Mutants

Each mutant replaced exactly one match, ran the single test method, and was reverted with git checkout. The tree was clean afterwards.

# Mutant Result Meaning
A2 (PR) ProductRepository.GetByIdAsync → AsNoTracking() red: CatalogRepositories.cs: GetByIdAsync Reproduced. The new discovery covers it.
A3 (PR) FeedUsageRepository gains a tracked GetByIdAsync red: FeedUsageRepository.cs is listed as an append-only ledger but now has a single-entity read (GetByIdAsync)… Reproduced.
M1 (own) FeedUsageRepository gains a live public void Update(FeedUsage entity) => db.FeedUsages.Update(entity); and still has no read green The list says "append-only" but never checks it. See F1.
M2 (own) EggUnitConversionRepository.GetByIdAsync → AsNoTracking(). This read feeds UpdateEggUnitConversionHandler's write. green A gap that predates this PR: the repository has no AddAsync, Update or Remove, so neither ref discovers it. See O1.

Findings

F1: P3. The append-only exclusion verifies "no read" but not "append-only". CONFIRMED.

  • Where: tests/Cluckwork.Api.IntegrationTests/TrackedMutationReadTests.cs:108-114 (the list) and :142-148 (its branch).
  • Defect: A listed file is excused when it has no single-entity read. Nothing checks that it still has no Update or Remove, which is the premise the list's name and comment state ("add rows but never read one back to change it").
  • Scenario: Someone makes feed usage editable and adds Update(FeedUsage) to the port and the repository. They feed it a stub built from the request instead of a read. That is the detached-write shape feat(tenancy): write-side tenant guard + single-assignment TenantContext (#546) #561 describes, and the guard stays green.
  • Evidence: Mutant M1 stays green.
  • Not a regression, and low risk:
  • Fix (about 2 lines): In the ledger branch, also assert that the file has no public void Update|Remove. For example, assert that entityTypes came only from AddAsync matches. The list name then verifies itself.

F2: P3. A comment names the deleted type. CONFIRMED.

  • Where: src/Cluckwork.Application/Features/Sales/ISalesOrderAllocationRepository.cs:5.
  • Defect: The comment says // Deliberately not IRepository: allocations are written as a batch…, and that type no longer exists. The diff made the comment stale but does not touch the file.
  • Fix: Reword the lead without the type name, for example "Deliberately has no single-entity Get, Update or Remove: …". Keep the rest.

F3: P3. The tech spec still lists IRepository as a port. CONFIRMED.

  • Where: specs/technical/tech_spec.md:46.
  • Defect: The line reads "port interfaces (IRepository, IIdentityProvider, IClock, IUnitOfWork)".
  • Fix: Name per-feature repository ports instead, for example IFlockRepository.
  • Also stale, but leave it: docs/decisions/849-module-contract.md:72 ("as every IRepository<T, TId> does"). It is a decision record and records history.

F4: P3. The guard's own comments still assume Update/Remove. CONFIRMED.

  • Where: TrackedMutationReadTests.cs:80-84.
  • Defect: The comment says the ReadOnly reads "are never fed to Update/Remove". IDailyEntryRepository and ISalesOrderRepository no longer have either member, so the reason has lost its subject.
  • Fix: Reword to "never fed to a write".
  • Related: :92-94 ("Most repositories lost their unused Update and Remove in [C] #514 slice 16: Platform composition, jobs, CLI and seeder conversion #858…") describes history, not the invariant. One clause saying that AddAsync names the aggregate a repository owns would carry the same rule.

F5: P3. The squash mixes in an unrelated docs commit. PLAUSIBLE.

  • Where: commit e8e46067 (src/AGENTS.md:10).
  • Defect: This commit holds the test(arch): declare the Insights module contract in the ledger #1040 Insights review fixes. On squash-merge it lands under the title refactor(repositories): …, so the Insights rule change is not visible from the release note.
  • Mitigation: The PR body discloses the commit, and I checked its two claims: Program.cs:78 registers IInsightsModule, and IInsightsModule : IReportQueries, IExportQueries.
  • Options: Accept it as disclosed, or split it out. This is the owner's call.

Observations (not findings against this PR)

  • O1: tracked-read coverage gap that predates this PR. CONFIRMED by M2.
    • Gap: The guard keys on AddAsync, Update and Remove. It therefore never sees EggUnitConversionRepository.GetByIdAsync, which feeds UpdateEggUnitConversionHandler.cs:16. If that read became AsNoTracking, the handler would report success and silently persist nothing. ProductEggGradeMapping is also unseen, because AddMappingAsync does not match the pattern.
    • Comment overreach: The rewritten header comment ("every repository that writes an entity") claims more than its parenthetical delivers.
    • Follow-up option: Key on the DbSet<T> entity types in AppDbContext instead (see the thermo section). Measured cost: 4 untracked AccountRepository reads that are not named ReadOnly would need exclusions or renames (GetCurrentAsync, GetCurrentSharedLockedAsync, GetCurrentLockedAsync, FindBySlugAsync). That is out of scope for a deletion slice.
  • O2: no narrowed port is left with a single member. The smallest are IFeedUsageRepository (AddAsync, ListAsync) and IInventoryMovementRepository (AddAsync, ListByItemAsync). Folding either into its module facade would read worse: it would put an entity (FeedUsage, InventoryMovement) on a contract, which ModuleContractRealAssemblyTests rejects. The repo's single-member ports (IMortalityLedger, IDailyEntryLookup, IEggUnitConversionLookup) are deliberate peer ports from [C] #514 slice 10: Flock Management contract, eligibility and mortality ports #852, [C] #514 slice 11: Egg Operations contract and the opaque stock seam #853 and [C] #514 slice 12: Commerce contract, consuming the stock seam #854, and this PR did not create them.
  • O3: IFlockRepository.Update is redundant with change tracking. All 4 callers load through the tracked GetByIdAsync. Deleting Update would still change SQL: DbSet.Update marks every property modified, so the UPDATE would go from all columns to changed columns only. Graph-attach semantics would change too. Keeping it is right for a slice that promises no SQL change. If anyone deletes it later, that belongs in its own PR with an SQL-trace parity check.

Code quality: pstack:thermo-nuclear-code-quality-review (P3, slice-scoped)

Verdict: a structural improvement with no regression. The PR removes a concept: a generic base that gave 15 ports four members whether or not anything used them. The net change is −69 lines. No file nears 1k lines (TrackedMutationReadTests.cs is 202). Production code gains no new branches.

Re-declaring the two members is the right shape, not duplication to extract. Each of the 11 surviving ports now spells out GetByIdAsync and AddAsync. Pulling those two lines back into a shared generic base would rebuild the abstraction this PR deletes, and would hand them back to the 4 ports that need only AddAsync. The per-port signatures are the honest contract.

Is the exclusion list a magic list? No. It is the boring, correct choice. The guard has one new branch, the ledger continue. The obvious alternative is to skip quietly any file that has AddAsync but no read. That would exempt exactly the files the anti-rot backstop exists to watch, and a pattern that later stopped matching would pass without notice. The list is the price of keeping the backstop. Each entry is checked mechanically in both directions:

  • a listed file that gains a read fails (A3);
  • a file dropped from the list fails through the backstop (PR mutant A4);
  • a stale entry fails Assert.All(..., Contains) (PR mutant A5).

The one weakness is that the name claims "append-only" without checking it (F1).

Code-judo move considered and deferred. The guard's real question is whether a persisted aggregate's single-entity read is tracked. It answers through a proxy: "this file has a mutation-shaped member". Keying directly on the DbSet<T> types in AppDbContext would make three pieces disappear: the mutates regex, the ledger list and its branch, and the backstop's special case. It would also close O1. It is not free, though. It needs a matching list of 4 untracked AccountRepository reads, and it widens the guard's scope. That suits a follow-up, not a deletion PR.

Comments. The new port comments on IBirdMovementRepository and IInventoryMovementRepository state a domain rule (corrections are Adjustment rows), so they earn their place. The test-local IStore comment says why the copy exists, which is also fine. F2 and F4 are the comment hygiene items.

Code quality: ponytail:ponytail-review (P3, slice-scoped)

  • tests/Cluckwork.Application.Tests/Architecture/SeamSurfaceTests.cs:L157-165: shrink. IStore<T,TId> mirrors all 4 members and both constraints, but the fixture only needs one inherited member that exposes T, for example public interface IStore<T> { Task<T?> GetByIdAsync(Guid id); }. That saves about 6 lines. This is optional: the faithful copy keeps both parameter and return positions in play, and that is a defensible reason to keep it.
  • tests/Cluckwork.Api.IntegrationTests/TrackedMutationReadTests.cs:L92-94: shrink. Drop the [C] #514 slice 16: Platform composition, jobs, CLI and seeder conversion #858 history sentence and keep the rule. About −2 lines.
  • src/Cluckwork.Application/Features/Flocks/IFlockRepository.cs:L174 (diff): yagni, deferred. Update is redundant with the tracked read (O3). Do not cut it in this slice, because doing so changes SQL.
  • Everything else is deletions, or contract lines that callers need. The append-only list is load-bearing, not speculative.

net: -8 lines possible. Lean otherwise.

Tests run (head 3117bdbd, one class at a time)

  • Build: dotnet build Cluckwork.sln: 0 warnings, 0 errors.
  • Application.Tests:
    • SeamSurfaceRealAssemblyTests: 2/2
    • SeamSurfaceTests: 39/39
    • ModuleContractRealAssemblyTests: 1/1
    • AdapterReachTests: 97/97
    • ModuleLedgerRealTreeTests: 2/2
    • CouplingMatrixRealTreeTests: 2/2
    • PeerContractRealTreeTests: 1/1
    • InsightsReadOnlyTests: 23/23
    • Fake-carrying classes: UpdateCustomerHandlerTests 5, EggOperationsModuleTests 9, FinanceModuleTests 3, FlockLookupTests 10, MortalityLedgerTests 3, InventoryModuleTests 6, CommerceModuleTests 5. All green.
  • Integration: TrackedMutationReadTests: 2/2, run under sg docker.
  • Not run: the full Application suite and the full integration suite, because memory is limited. When I checked, CI's Tests (integration) was still pending and every other check had passed.

Verdict

Approve. There is no product defect, every guard keeps its purpose, and the tracked-read walk is now a strict superset of main's. F1 to F4 are optional P3 polish: a 2-line self-check plus three stale comments and doc lines. F5 is a scope call for the owner.

@mforce

mforce commented Oct 3, 2026

Copy link
Copy Markdown
Owner Author

Review of record (Codex Astra, xhigh) at head 3117bdbd. Posted by the coordinator.

Reviewed PR #1042 at 3117bdbd9ac3852be8e6c94ec3a6ae5453356256 against 922b208d, including the #1040 documentation commit. Review performed without delegation.

One P2 guard defect needs correction. No product defect found. The production deletion is consistent with its callers, and the restored head builds cleanly with 213 selected tests passing.

Finding F1: P2, CONFIRMED. tests/Cluckwork.Api.IntegrationTests/TrackedMutationReadTests.cs:142 exempts a named ledger even after it gains an explicit Update or Remove. The branch checks only reads.Count == 0, then skips the existing requirement that a mutable repository have a tracked single-entity read. It never verifies the premise that the excluded repository still only appends rows.

The failure scenario is a later change adding a destructive operation to one of these four ports and implementations. A caller could pass a row from the existing untracked list, or an attached stub, without introducing a guarded single-entity load. The new exclusion leaves the tracked-read backstop green despite the repository no longer being append-only. This is a guard defect, not an existing deletion path or a demonstrated tenant-isolation defect.

I reproduced it with the unlisted N1 mutant: add void Remove(FeedUsage entity) to IFeedUsageRepository, implement it as db.FeedUsages.Remove(entity), and supply the required member on FakeFeedUsages. The whole solution builds with zero warnings/errors, and both TrackedMutationReadTests pass. The ordinary writer regex recognizes the added method; the exclusion bypasses its backstop. See the mutation diff, build result, and test result.

Require an excluded file to have no recognized Update/Remove declaration before taking the exclusion, or route those declarations through the ordinary mutable-repository checks. Keep the empty-read and stale-entry assertions. This needs a small condition using the existing discovery information, not a scanner rewrite. Repeat N1 to prove it fails.

Deletion and guard coverage.

  • The production implementations lose exactly 15 Remove, 14 Update, and four GetByIdAsync methods. The ports retain 15 AddAsync, 11 GetByIdAsync, and the live IFlockRepository.Update. Its four lifecycle-handler calls remain. The unrelated live assignment and logo Remove methods remain.
  • Whole-solution compilation covers concrete calls, explicit implementations, test doubles and the counting decorator. Searches found no production reflection or dynamic invocation of the deleted methods. DI uses explicit port/implementation registrations rather than a generic-repository scan. Seeder and CLI paths do not invoke a deleted member.
  • The six throwing stubs belonged to BirdMovement, FeedUsage and InventoryMovement. Their domain APIs remain create-only; corrections still append compensating rows. No live caller relied on the exceptions, and no void implementation was deleted. The current ports make those calls unavailable at compile time. F1 concerns a later reintroduction.
  • Both assembly anchors now use IUnitOfWork in the same Application assembly. The two seam-assembly checks and the contract-assembly check pass without lowering their coverage requirements.
  • AdapterReachTests.cs:447 retains its unbound generic shape. IStore<Flock, Guid> still makes the scanner resolve Flock through the using and longest owner claim; all 97 adapter fixture tests pass.
  • The local IStore<T,TId> in SeamSurfaceTests preserves the inherited generic interface, constraints and four signatures. B1 produces the same three failures on main and head, including Contract_RepositoryInAResult_IsAViolation. The restored class passes all 39 cases.
  • The independent census confirms 16 files/22 reads on main and 18 files/19 reads at head. No previously discovered file disappears. The four missing reads are exactly the deleted methods; Product adds one newly guarded read. All four excluded ledgers currently satisfy their stated purpose. A3 catches a conventional new single-entity read, and A5 proves the stale-entry check actually fails. F1 is the missing check on their writer methods.

Mutation results. All mutations were reverted.

Mutant Main 922b208d Head 3117bdbd
A2: Product GetByIdAsync uses AsNoTracking Green, 2/2 Red, names CatalogRepositories.cs: GetByIdAsync
A3: FeedUsage gains GetByIdAsync Not run Red, names its append-only exclusion
A5: add nonexistent RetiredLedgerRepository.cs exclusion Not run Red, stale-entry assertion
B1: stop walking inherited interfaces inside contract results Same 3 failures, 36 passes Same 3 failures, 36 passes
N1: restore real FeedUsage Remove on port and implementation Not run Green, 2/2; solution also builds

A2-head and A3-head ran the compiled head guard against mutated source with --no-build, because this assertion reads source files. Mutations to the guard itself were rebuilt. A2-main ran against the actual main checkout with a rebuild. N1 rebuilt the entire solution. Commands, patches, logs and the discovery census are retained in the verification record.

#1040 documentation fold-in. src/AGENTS.md:27 is accurate. IInsightsModule inherits both query ports; neither query port is on Insights' contract list. The adapter/peer check authorizes named types from that list, so adding either would permit direct injection around the facade. Program.cs:78 registers IInsightsModule; the new exception to “registered beside them” matches the implementation and the agreed P7 plan.

Nits, separate from F1.

  • P3, CONFIRMED: src/Cluckwork.Application/Features/Sales/ISalesOrderAllocationRepository.cs:5 still says “Deliberately not IRepository.” The deletion makes that explanation refer to a nonexistent production type. A maintainer following it has no definition to inspect. Replace that clause with the existing batch-write explanation. Evidence is the remaining comment and deletion of Common/IRepository.cs; no product impact.

Code quality. The strict maintainability pass found no additional structural regression. Production changes remove behavior from the ports and implementations without adding wrappers, dependencies or replacement abstractions. No changed C# file crosses 1,000 lines; the largest is the unchanged-length 853-line adapter fixture file. The test-local generic interface preserves measured coverage.

ponytail:ponytail-review, complexity only: “Lean already. Ship.” No complexity cuts recommended. F1 remains a correctness finding about the guard.

Final verification at the restored head: solution build 0 warnings, 0 errors; 180 Application tests and 33 integration tests passed, across 15 sequential class runs. Integration coverage was TrackedMutationReadTests 2, SaleAllocationPolicyTests 13, BirdMovementTests 5 and FeedUsageTests 13. The full integration suite was not run. No commits, pushes or GitHub comments were made. The clean assigned review checkout was removed as requested.

Verdict: REQUEST CHANGES for F1's append-only exclusion check; no current product defect found.

mforce added 2 commits October 3, 2026 20:30
…ate or Remove

TrackedMutationReadTests excused the four named append-only ledgers once
they had no single-entity read, but never checked that they were still
append-only. A ledger that gained a real Update or Remove (Astra's N1)
stayed green. The exclusion now also refuses a listed file that declares
either, so its reads go back through the ordinary checks.

Also fixes the stale IRepository mentions (Opus F2, F3) and the guard's
comments that still assumed Update/Remove (Opus F4).

Refs #858
@mforce

mforce commented Oct 3, 2026

Copy link
Copy Markdown
Owner Author

Reply to the Codex Astra review, fixed in 39151656.

F1 (P2): fixed. In the append-only branch, TrackedMutationReadTests now refuses a listed ledger that declares Update or Remove. The writer regex gained a named edit group, so the check reuses the existing discovery matches and the scanner is not rewritten. The empty-read and stale-entry assertions are unchanged.

Your N1 mutant is now red. I applied your diff (port, implementation, test fake) and reran the guard:

FeedUsageRepository.cs is listed as an append-only ledger but now declares Remove. Remove it from the list so its reads are checked like any other mutable repository.

An Update variant (Opus's M1) is red the same way. With the mutants reverted the guard is 2 of 2 green, and the Application suite is 702 of 702.

Nit (ISalesOrderAllocationRepository.cs:5): fixed. The comment now reads "Deliberately has no single-entity Get, Update or Remove: …".

@mforce

mforce commented Oct 3, 2026

Copy link
Copy Markdown
Owner Author

Reply to the Claude Opus review, fixed in 39151656.

  • F1: fixed. A listed append-only ledger that declares Update or Remove now fails the exemption, so its reads are checked again. Your M1 (Update(FeedUsage)) and Astra's N1 (Remove(FeedUsage)) are both red: FeedUsageRepository.cs is listed as an append-only ledger but now declares Update.
  • F2: fixed. ISalesOrderAllocationRepository.cs:5 no longer names the deleted type.
  • F3: fixed. specs/technical/tech_spec.md:46 now says "per-feature repositories such as IFlockRepository". 849-module-contract.md:72 is left as history, as you suggested.
  • F4: fixed.
  • F5: left as disclosed, per the owner's scope.
  • O1 / M2: not fixed, follow-up. EggUnitConversionRepository.GetByIdAsync and ProductEggGradeMapping stay outside discovery: the first has no AddAsync/Update/Remove, and the second writes through AddMappingAsync. It predates this PR and does not fit in one line of the guard. It is recorded in the PR body.
  • Ponytail shrink of IStore: not taken. The faithful copy keeps both parameter and return positions, as you noted.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant